Compare commits

...
16 changed files with 126 additions and 76 deletions
@@ -256,18 +256,20 @@ public class LocalResourceController implements RMController {
return new RMProject[0];
}
var projects = new ArrayList<RMProject>();
var allPaths = Files.list(sharedProjectsPath).collect(Collectors.toList());
for (Path path : allPaths) {
var projectPerms = getProjectPermissions(
makeProjectIdFromPath(path, RMProjectType.SHARED),
RMProjectType.SHARED
);
var rmProject = makeProjectFromPath(path, projectPerms, RMProjectType.SHARED, false);
projects.add(rmProject);
try (Stream<Path> list = Files.list(sharedProjectsPath)) {
var allPaths = list.toList();
for (Path path : allPaths) {
var projectPerms = getProjectPermissions(
makeProjectIdFromPath(path, RMProjectType.SHARED),
RMProjectType.SHARED
);
var rmProject = makeProjectFromPath(path, projectPerms, RMProjectType.SHARED, false);
projects.add(rmProject);
}
return projects.stream()
.filter(Objects::nonNull)
.toArray(RMProject[]::new);
}
return projects.stream()
.filter(Objects::nonNull)
.toArray(RMProject[]::new);
} catch (IOException e) {
throw new DBException("Error reading shared projects", e);
}
@@ -581,7 +583,7 @@ public class LocalResourceController implements RMController {
@NotNull String resourcePath,
boolean isFolder
) throws DBException {
try (var projectLock = lockController.lockProject(projectId, "createResource")) {
try (var ignoredLock = lockController.lockProject(projectId, "createResource")) {
validateResourcePath(resourcePath);
Path targetPath = getTargetPath(projectId, resourcePath);
if (Files.exists(targetPath)) {
@@ -613,7 +615,7 @@ public class LocalResourceController implements RMController {
@NotNull String oldResourcePath,
@NotNull String newResourcePath
) throws DBException {
try (var projectLock = lockController.lockProject(projectId, "moveResource")) {
try (var ignoredLock = lockController.lockProject(projectId, "moveResource")) {
var normalizedOldResourcePath = CommonUtils.normalizeResourcePath(oldResourcePath);
var normalizedNewResourcePath = CommonUtils.normalizeResourcePath(newResourcePath);
if (log.isDebugEnabled()) {
@@ -679,7 +681,7 @@ public class LocalResourceController implements RMController {
@Override
public void deleteResource(@NotNull String projectId, @NotNull String resourcePath, boolean recursive) throws DBException {
try (var projectLock = lockController.lockProject(projectId, "deleteResource")) {
try (var ignoredLock = lockController.lockProject(projectId, "deleteResource")) {
if (log.isDebugEnabled()) {
log.debug("Removing resource from '" + resourcePath + "' in project '" + projectId + "'" + (recursive ? " recursive" : ""));
}
@@ -768,7 +770,7 @@ public class LocalResourceController implements RMController {
@NotNull byte[] data,
boolean forceOverwrite
) throws DBException {
try (var lock = lockController.lockProject(projectId, "setResourceContents")) {
try (var ignoredLock = lockController.lockProject(projectId, "setResourceContents")) {
validateResourcePath(resourcePath);
Number fileSizeLimit = WebAppUtils.getWebApplication()
.getAppConfiguration()
@@ -822,7 +824,7 @@ public class LocalResourceController implements RMController {
@NotNull String propertyName,
@Nullable Object propertyValue
) throws DBException {
try (var projectLock = lockController.lockProject(projectId, "resourcePropertyUpdate")) {
try (var ignoredLock = lockController.lockProject(projectId, "resourcePropertyUpdate")) {
validateResourcePath(resourcePath);
BaseWebProjectImpl webProject = getWebProject(projectId, false);
doFileWriteOperation(projectId, webProject.getMetadataFilePath(),
@@ -843,7 +845,7 @@ public class LocalResourceController implements RMController {
@NotNull String resourcePath,
@NotNull Map<String, Object> properties
) throws DBException {
try (var projectLock = lockController.lockProject(projectId, "resourcePropertyUpdate")) {
try (var ignoredLock = lockController.lockProject(projectId, "resourcePropertyUpdate")) {
validateResourcePath(resourcePath);
BaseWebProjectImpl webProject = getWebProject(projectId, false);
doFileWriteOperation(projectId, webProject.getMetadataFilePath(),
@@ -1110,7 +1112,7 @@ public class LocalResourceController implements RMController {
);
}
if (readProperties) {
final BaseProjectImpl project = (BaseProjectImpl) getWebProject(projectId, true);
final BaseProjectImpl project = getWebProject(projectId, true);
final String resourcePath = getProjectRelativePath(projectId, path);
final Map<String, Object> properties = project.getResourceProperties(resourcePath);
@@ -1155,7 +1157,7 @@ public class LocalResourceController implements RMController {
);
}
private void fireRmProjectAddEvent(@NotNull RMProject project) throws DBException {
private void fireRmProjectAddEvent(@NotNull RMProject project) {
RMEventManager.fireEvent(
new RMEvent(
RMEvent.Action.RESOURCE_ADD,
@@ -43,6 +43,7 @@ import java.nio.file.Path;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class RMNIOTest {
@@ -135,12 +136,14 @@ public class RMNIOTest {
String file2 = "script" + SecurityUtils.generateUniqueId() + ".sql";
rm.createResource(testProject.getId(), file1, true);
rm.createResource(testProject.getId(), file2, false);
Set<String> filesFromNio =
Files.list(rootPath)
.map(path -> ((RMPath) path).getResourcePath())
.collect(Collectors.toSet());
Assert.assertTrue(filesFromNio.contains(file1));
Assert.assertTrue(filesFromNio.contains(file2));
try (Stream<Path> list = Files.list(rootPath)) {
Set<String> filesFromNio =
list
.map(path -> ((RMPath) path).getResourcePath())
.collect(Collectors.toSet());
Assert.assertTrue(filesFromNio.contains(file1));
Assert.assertTrue(filesFromNio.contains(file2));
}
}
@Test
@@ -31,7 +31,7 @@ export class DataViewerDataChangeConfirmationService {
const model = this.dataViewerTableService.get(modelId);
if (model && !model.onRequest.hasHandler(this.checkUnsavedData)) {
model.onRequest.addHandler(executorHandlerFilter(({ operation }) => operation !== DatabaseDataSourceOperation.Save, this.checkUnsavedData));
model.onRequest.addHandler(executorHandlerFilter(({ operation }) => operation === DatabaseDataSourceOperation.Request, this.checkUnsavedData));
}
}
@@ -111,14 +111,7 @@ export class DataViewerTabService {
const model = this.tableViewerStorageService.get(tab.handlerState.tableId || '');
if (model) {
let canClose = false;
try {
await model.source.runTask(async () => {
canClose = true;
});
} catch {}
return canClose;
return await model.source.canSafelyDispose();
}
return true;
@@ -125,7 +125,7 @@ export class ResultSetDataContentAction extends DatabaseDataAction<any, IDatabas
throw new Error('Failed to get value metadata information');
}
const fullText = await this.source.runTask(async () => {
const fullText = await this.source.runOperation(async () => {
try {
this.updateCache(element, { loading: true });
return await this.loadFileFullText(this.result, column.position, row);
@@ -134,6 +134,10 @@ export class ResultSetDataContentAction extends DatabaseDataAction<any, IDatabas
}
});
if (fullText === null) {
throw new Error('Failed to get value metadata information');
}
this.updateCache(element, { fullText });
return fullText;
@@ -167,7 +171,7 @@ export class ResultSetDataContentAction extends DatabaseDataAction<any, IDatabas
this.clearCache();
}
private async getFileDataUrl(element: IResultSetElementKey) {
private async getFileDataUrl(element: IResultSetElementKey): Promise<string> {
const column = this.data.getColumn(element.column);
const row = this.data.getRowValue(element.row);
@@ -175,7 +179,7 @@ export class ResultSetDataContentAction extends DatabaseDataAction<any, IDatabas
throw new Error('Failed to get value metadata information');
}
const url = await this.source.runTask(async () => {
const url = await this.source.runOperation(async () => {
try {
this.updateCache(element, { loading: true });
return await this.loadDataURL(this.result, column.position, row);
@@ -184,6 +188,10 @@ export class ResultSetDataContentAction extends DatabaseDataAction<any, IDatabas
}
});
if (url === null) {
throw new Error('Failed to get value metadata information');
}
return url;
}
@@ -65,7 +65,7 @@ export abstract class DatabaseDataSource<TOptions, TResult extends IDatabaseData
private get activeOperation(): Promise<void> | null {
return this.activeOperationStack[this.activeOperationStack.length - 1] ?? null;
}
private readonly activeOperationStack: Array<Promise<void>>;
private readonly activeOperationStack: Array<Promise<any>>;
constructor(serviceInjector: IServiceInjector) {
this.serviceInjector = serviceInjector;
@@ -286,12 +286,12 @@ export abstract class DatabaseDataSource<TOptions, TResult extends IDatabaseData
await this.lastAction();
}
async runTask(task: () => Promise<any>): Promise<any> {
async runOperation<T>(task: () => Promise<T>): Promise<T | null> {
return this.tryExecuteOperation(DatabaseDataSourceOperation.Task, task);
}
async requestData(mutation?: () => void): Promise<void> {
await this.tryExecuteOperation(DatabaseDataSourceOperation.Load, () => {
await this.tryExecuteOperation(DatabaseDataSourceOperation.Request, () => {
this.lastAction = this.requestData.bind(this);
mutation?.();
@@ -300,7 +300,7 @@ export abstract class DatabaseDataSource<TOptions, TResult extends IDatabaseData
}
async requestDataPortion(offset: number, count: number): Promise<void> {
await this.tryExecuteOperation(DatabaseDataSourceOperation.Load, () => {
await this.tryExecuteOperation<TResult[] | void>(DatabaseDataSourceOperation.Request, () => {
if (!this.isDataAvailable(offset, count)) {
this.lastAction = this.requestDataPortion.bind(this, offset, count);
@@ -312,7 +312,7 @@ export abstract class DatabaseDataSource<TOptions, TResult extends IDatabaseData
}
async refreshData(): Promise<void> {
await this.tryExecuteOperation(DatabaseDataSourceOperation.Refresh, () => {
await this.tryExecuteOperation(DatabaseDataSourceOperation.Request, () => {
this.lastAction = this.refreshData.bind(this);
if (this.prevOptions) {
@@ -333,6 +333,15 @@ export abstract class DatabaseDataSource<TOptions, TResult extends IDatabaseData
});
}
async canSafelyDispose(): Promise<boolean> {
try {
const result = await this.tryExecuteOperation(DatabaseDataSourceOperation.Request, async () => true);
return result || false;
} catch {
return false;
}
}
clearError(): this {
this.error = null;
return this;
@@ -358,7 +367,7 @@ export abstract class DatabaseDataSource<TOptions, TResult extends IDatabaseData
abstract loadTotalCount(resultIndex: number): Promise<ITask<number>>;
abstract cancelLoadTotalCount(): Promise<ITask<number> | null>;
private async requestDataAction(): Promise<TResult[] | null> {
private async requestDataAction(): Promise<TResult[]> {
this.prevOptions = toJS(this.options);
return this.request(this.results).then(data => {
this.outdated = false;
@@ -370,8 +379,8 @@ export abstract class DatabaseDataSource<TOptions, TResult extends IDatabaseData
});
}
private async tryExecuteOperation(type: DatabaseDataSourceOperation, operation: () => Promise<any>): Promise<void> {
if (this.activeOperation && type !== DatabaseDataSourceOperation.Task && type !== DatabaseDataSourceOperation.Save) {
private async tryExecuteOperation<T>(type: DatabaseDataSourceOperation, operation: () => Promise<T>): Promise<T | null> {
if (this.activeOperation && type !== DatabaseDataSourceOperation.Request) {
await this.activeOperation;
}
@@ -387,23 +396,25 @@ export abstract class DatabaseDataSource<TOptions, TResult extends IDatabaseData
}
}
private executeOperation(type: DatabaseDataSourceOperation, operation: () => Promise<void> | void): ITask<void> {
private executeOperation<T>(type: DatabaseDataSourceOperation, operation: () => Promise<T> | T): ITask<T | null> {
return new Task(async () => await this.onOperation.execute({ stage: 'request', operation: type })).run().then(contexts => {
// TODO: maybe it's better to throw an exception instead, so we will not have unexpected undefined results
if (ExecutorInterrupter.isInterrupted(contexts)) {
return;
return null;
}
return new Task(async () => await this.onOperation.execute({ stage: 'before', operation: type }))
.run()
.then(contexts => {
if (ExecutorInterrupter.isInterrupted(contexts)) {
return;
return null;
}
return operation();
})
.then(async () => {
.then(async result => {
await this.onOperation.execute({ stage: 'after', operation: type });
return result;
});
});
}
@@ -15,10 +15,12 @@ import type { IDatabaseDataActions } from './IDatabaseDataActions';
import type { IDatabaseDataResult } from './IDatabaseDataResult';
export enum DatabaseDataSourceOperation {
Load = 'load',
Save = 'save',
Refresh = 'refresh',
/** Abstract operation with data, should not lead to data lost */
Task = 'task',
/** Saving operation */
Save = 'save',
/** May lead to data lost */
Request = 'request',
}
export interface IDatabaseDataSourceOperationEvent {
stage: 'request' | 'before' | 'after';
@@ -102,15 +104,19 @@ export interface IDatabaseDataSource<TOptions, TResult extends IDatabaseDataResu
cancelLoadTotalCount: () => Promise<ITask<number> | null>;
retry: () => Promise<void>;
/** Allows to perform an asynchronous action on the data source, this action will wait previous action to finish and save or load requests.
* The data source will have a loading and disabled state while performing an action */
runTask: <T>(task: () => Promise<T>) => Promise<T>;
/**
* Perform operation with data source. This action should not lead to data lost. Can be cancelled when operation is Task.
* @param operation Task or Promise
* @returns
*/
runOperation: <T>(operation: () => Promise<T>) => Promise<T | null>;
requestDataPortion(offset: number, count: number): Promise<void>;
requestData: (mutation?: () => void) => Promise<void> | void;
refreshData: () => Promise<void> | void;
saveData: () => Promise<void> | void;
cancel: () => Promise<void> | void;
requestData: (mutation?: () => void) => Promise<void>;
refreshData: () => Promise<void>;
saveData: () => Promise<void>;
cancel: () => Promise<void>;
clearError: () => this;
resetData: () => this;
canSafelyDispose: () => Promise<boolean>;
dispose: (keepExecutionContext?: boolean) => Promise<void>;
}
@@ -7,4 +7,10 @@
*/
import { ACTION_REFRESH, createMenu } from '@cloudbeaver/core-view';
export const MENU_DATA_VIEWER_AUTO_REFRESH = createMenu('auto-refresh', 'Auto-Refresh', undefined, 'data_viewer_action_auto_refresh', ACTION_REFRESH);
export const MENU_DATA_VIEWER_AUTO_REFRESH = createMenu(
'auto-refresh',
'data_viewer_action_auto_refresh',
undefined,
'data_viewer_action_auto_refresh',
ACTION_REFRESH,
);
@@ -13,7 +13,7 @@ import { Bootstrap, injectable } from '@cloudbeaver/core-di';
import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs';
import { LocalizationService } from '@cloudbeaver/core-localization';
import { declensionOfNumber } from '@cloudbeaver/core-utils';
import { ACTION_REFRESH, ActionService, MenuBaseItem, menuExtractItems, MenuService } from '@cloudbeaver/core-view';
import { ACTION_REFRESH, ActionService, MenuBaseItem, menuExtractItems, MenuSeparatorItem, MenuService } from '@cloudbeaver/core-view';
import { IDatabaseRefreshState } from '../../../../DatabaseDataModel/Actions/DatabaseRefreshAction';
import { DATA_CONTEXT_DV_DDM } from '../../../../DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM';
@@ -72,12 +72,13 @@ export class TableRefreshActionBootstrap extends Bootstrap {
const state = getRefreshState(context);
items = [...items];
for (const interval of AUTO_REFRESH_INTERVALS) {
const label = this.getLabel(interval);
items.push(
new MenuBaseItem(
{
id: `auto-refresh-${interval}`,
label: this.getLabel(interval),
tooltip: 'data_viewer_action_auto_refresh',
label,
tooltip: label,
disabled: state?.interval === interval * 1000,
},
{
@@ -89,12 +90,14 @@ export class TableRefreshActionBootstrap extends Bootstrap {
);
}
items.push(new MenuSeparatorItem());
items.push(
new MenuBaseItem(
{
id: 'auto-refresh-custom',
label: 'ui_custom',
tooltip: 'data_viewer_action_auto_refresh',
tooltip: 'plugin_data_viewer_action_auto_refresh_custom',
},
{
onSelect: this.configureAutoRefresh.bind(this, context),
@@ -102,6 +105,22 @@ export class TableRefreshActionBootstrap extends Bootstrap {
),
);
items.push(
new MenuBaseItem(
{
id: 'auto-refresh-stop',
label: 'ui_processing_stop',
tooltip: 'data_viewer_action_auto_refresh_stop',
disabled: !state?.isAutoRefresh,
},
{
onSelect: () => {
state?.setInterval(0);
},
},
),
);
return items;
},
});
@@ -21,6 +21,7 @@ export default [
['data_viewer_action_refresh', 'Refresh'],
['data_viewer_action_auto_refresh', 'Auto-Refresh'],
['data_viewer_action_auto_refresh_stop', 'Stop auto-refresh'],
['plugin_data_viewer_action_auto_refresh_custom', 'Configure auto-refresh'],
['data_viewer_action_edit_delete', 'Delete selected'],
['data_viewer_action_edit_add', 'Add'],
['data_viewer_action_edit_add_copy', 'Duplicate'],
@@ -28,7 +29,7 @@ export default [
['data_viewer_result_edited_title', 'Save changes'],
['data_viewer_result_edited_message', 'Result set was edited. Do you want to save changes in database?'],
['data_viewer_data_save_error_title', 'Error occurred while saving changes'],
['data_viewer_auto_refresh_settings', 'Auto refresh Settings'],
['data_viewer_auto_refresh_settings', 'Auto-Refresh Settings'],
['data_viewer_auto_refresh_settings_stop_on_error', 'Stop on error'],
['data_viewer_presentation_value_no_active_elements', 'No selected table cells'],
['data_viewer_presentation_value_title', 'Value'],
@@ -21,6 +21,7 @@ export default [
['data_viewer_action_refresh', 'Actualiser'],
['data_viewer_action_auto_refresh', 'Actualisation automatique'],
['data_viewer_action_auto_refresh_stop', "Arrêter l'actualisation automatique"],
['plugin_data_viewer_action_auto_refresh_custom', 'Configure auto-refresh'],
['data_viewer_action_edit_delete', 'Supprimer la sélection'],
['data_viewer_action_edit_add', 'Ajouter'],
['data_viewer_action_edit_add_copy', 'Dupliquer'],
@@ -21,10 +21,11 @@ export default [
['data_viewer_action_refresh', 'Refresh'],
['data_viewer_action_auto_refresh', 'Auto-Refresh'],
['data_viewer_action_auto_refresh_stop', 'Stop auto-refresh'],
['plugin_data_viewer_action_auto_refresh_custom', 'Configure auto-refresh'],
['data_viewer_result_edited_title', 'Save changes'],
['data_viewer_result_edited_message', 'Result set was edited. Do you want to save changes in database?'],
['data_viewer_data_save_error_title', 'Error occurred while saving changes'],
['data_viewer_auto_refresh_settings', 'Auto refresh Settings'],
['data_viewer_auto_refresh_settings', 'Auto-Refresh Settings'],
['data_viewer_auto_refresh_settings_stop_on_error', 'Stop on error'],
['data_viewer_presentation_value_no_active_elements', 'No selected table cells'],
['data_viewer_presentation_value_title', 'Valore'],
@@ -21,6 +21,7 @@ export default [
['data_viewer_action_refresh', 'Обновить'],
['data_viewer_action_auto_refresh', 'Автообновление'],
['data_viewer_action_auto_refresh_stop', 'Остановить автообновление'],
['plugin_data_viewer_action_auto_refresh_custom', 'Настроить автообновление'],
['data_viewer_action_edit_delete', 'Удалить'],
['data_viewer_action_edit_add', 'Добавить'],
['data_viewer_action_edit_add_copy', 'Дублировать'],
@@ -21,6 +21,7 @@ export default [
['data_viewer_action_refresh', 'Refresh'],
['data_viewer_action_auto_refresh', 'Auto-Refresh'],
['data_viewer_action_auto_refresh_stop', 'Stop auto-refresh'],
['plugin_data_viewer_action_auto_refresh_custom', 'Configure auto-refresh'],
['data_viewer_action_edit_delete', '删除选中'],
['data_viewer_action_edit_add', '添加'],
['data_viewer_action_edit_add_copy', 'Duplicate'],
@@ -28,7 +29,7 @@ export default [
['data_viewer_result_edited_title', '保存更改'],
['data_viewer_result_edited_message', '结果集已编辑。是否将更改保存到数据库?'],
['data_viewer_data_save_error_title', 'Error occurred while saving changes'],
['data_viewer_auto_refresh_settings', 'Auto refresh Settings'],
['data_viewer_auto_refresh_settings', 'Auto-Refresh Settings'],
['data_viewer_auto_refresh_settings_stop_on_error', 'Stop on error'],
['data_viewer_presentation_value_no_active_elements', 'No selected table cells'],
['data_viewer_presentation_value_title', '值'],
@@ -136,14 +136,7 @@ export class SqlQueryResultService {
const model = this.tableViewerStorageService.get(group.modelId);
if (model) {
let canClose = false;
try {
await model.source.runTask(async () => {
canClose = true;
});
} catch {}
return canClose;
return await model.source.canSafelyDispose();
}
}
return true;
@@ -24,7 +24,11 @@ export class ScriptPreviewService {
async open(model: IDatabaseDataModel, resultIndex: number): Promise<void> {
try {
const script = await model.source.runTask(() => this.tryGetScript(model, resultIndex));
const script = await model.source.runOperation(() => this.tryGetScript(model, resultIndex));
if (script === null) {
throw new Error('Script is not provided');
}
this.commonDialogService.open(ScriptPreviewDialog, {
script,