Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion chat2db-community-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"build": "yarn run build:web:community",
"build:prod": "yarn run build:web:community",
"build:web": "umi build",
"prebuild:web:community": "yarn test:community-boundary && yarn test:database-capabilities && yarn test:table-editor && yarn test:retired-ai && yarn test:tree-title-highlight && yarn test:tree-search-lifecycle && yarn test:driver-upload && yarn test:tree-loading && yarn test:tree-node-lookup && yarn test:tree-node-path && yarn test:data-source-authorization && yarn test:active-transactions && yarn test:data-source-mutation-refresh && yarn test:ai-model-config && yarn test:ai-model-select && yarn test:export-connections && yarn test:main-page-navigation && yarn test:shortcut && yarn test:console-tab-name && yarn test:workspace-tab-scroll && yarn test:workspace-split-lifecycle && yarn test:sql-completion-context && yarn test:file-manager-label && yarn test:local-file-encoding && yarn test:local-file-save && yarn test:local-file-tree-path && yarn test:saved-console-dirty && yarn test:editor-close && yarn test:result-set-ui && yarn test:result-pagination && yarn test:sql-execution-stream && yarn test:result-table-layout && yarn test:result-status && yarn test:data-source-identity && yarn test:data-source-execution-snapshot && yarn test:data-source-watermark && yarn test:hot-update && yarn test:task-center && yarn test:application-exit && yarn test:monaco-lifecycle && yarn test:result-set-editor && yarn test:account-grants-request",
"prebuild:web:community": "yarn test:community-boundary && yarn test:database-capabilities && yarn test:table-editor && yarn test:retired-ai && yarn test:tree-title-highlight && yarn test:tree-search-lifecycle && yarn test:driver-upload && yarn test:tree-loading && yarn test:tree-node-lookup && yarn test:tree-node-path && yarn test:data-source-authorization && yarn test:active-transactions && yarn test:data-source-mutation-refresh && yarn test:ai-model-config && yarn test:ai-model-select && yarn test:export-connections && yarn test:main-page-navigation && yarn test:shortcut && yarn test:console-tab-name && yarn test:workspace-tab-scroll && yarn test:workspace-split-lifecycle && yarn test:sql-completion-context && yarn test:file-manager-label && yarn test:local-file-encoding && yarn test:local-file-save && yarn test:local-file-tree-path && yarn test:saved-console-dirty && yarn test:editor-close && yarn test:result-set-ui && yarn test:result-pagination && yarn test:sql-execution-stream && yarn test:result-table-layout && yarn test:result-status && yarn test:data-source-identity && yarn test:data-source-execution-snapshot && yarn test:data-source-watermark && yarn test:hot-update && yarn test:task-center && yarn test:application-exit && yarn test:monaco-lifecycle && yarn test:result-set-editor && yarn test:account-grants-request && yarn test:dashboard-store",
"postbuild:web:community": "node ./scripts/verify-production-bundles.cjs",
"build:web:2java": "yarn run build:web:community && rm -rf ../chat2db-community-server/chat2db-community-start/src/main/resources/thymeleaf/* && cp -r dist/index.html ../chat2db-community-server/chat2db-community-start/src/main/resources/thymeleaf/",
"build:web:community": "cross-env UMI_ENV=community cross-env APP_NAME=chat2db-community cross-env APP_VERSION=${npm_config_app_version} cross-env PRINT_LOGS=${npm_config_print_logs} cross-env APP_PORT=${npm_config_app_port} umi build",
Expand All @@ -30,6 +30,7 @@
"test:retired-ai": "tsx src/blocks/AI/retiredAiSurface.test.ts && tsx src/blocks/AI/components/AIChatInput/mentionSelection.test.ts && tsx src/blocks/AI/knowledgeSelection.test.ts && tsx src/blocks/AI/messageNavigation.test.ts",
"test:hot-update": "tsx src/store/global/slices/hotUpdate/action.test.ts",
"test:active-transactions": "tsx src/blocks/NewTree/components/ActiveTransactionsContent/activeTransactionUtils.test.ts && tsx src/blocks/NewTree/treeMenuGrouping.test.ts",
"test:dashboard-store": "node scripts/run-dashboard-store-tests.cjs",
"test:result-pagination": "tsx src/blocks/SearchResult/components/ResultSet/pagination.test.ts",
"test:tree-node-path": "tsx src/blocks/NewTree/utils/treeNodePath.test.ts",
"test:ai-model-config": "tsx src/blocks/AI/components/AIModelConfigModal/modelConfigDefaults.test.ts",
Expand Down
25 changes: 25 additions & 0 deletions chat2db-community-client/scripts/run-dashboard-store-tests.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const { existsSync } = require('node:fs');
const { spawnSync } = require('node:child_process');

const testFiles = [
'src/store/dashboard/slices/common/dashboardDetailRequest.test.ts',
'src/store/dashboard/slices/common/dashboardMutation.test.ts',
'src/store/dashboard/slices/common/refreshCurrentDashboard.test.ts',
'src/blocks/BI/ChartCardBox/DingChartModal/pinChartToDashboard.test.ts',
];
const tsxCli = require.resolve('tsx/cli');
let executed = 0;

for (const testFile of testFiles) {
if (!existsSync(testFile)) continue;
executed += 1;
const result = spawnSync(process.execPath, [tsxCli, testFile], { stdio: 'inherit' });
if (result.status !== 0) {
process.exit(result.status ?? 1);
}
}

if (!executed) {
console.error('No dashboard store tests were found.');
process.exit(1);
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import {
captureDashboardChartDeleteMutation,
isDashboardMutationCurrent,
resolveDashboardChartDeleteTarget,
resolveDashboardMutationState,
} from './dashboardMutation';
import { StateCreator } from 'zustand';
import { DashboardStore } from '../../store';
import { CommonState } from './initialState';
Expand Down Expand Up @@ -79,14 +85,11 @@ export const createCommonAction: StateCreator<DashboardStore, [['zustand/devtool
updateDashboard: (dashboard) => {
if (!dashboard) return;
updateDashboard(dashboard).then(() => {
set({
currentDashboard: {
...(get().currentDashboard || {}),
...dashboard,
},
dashboardList: get().dashboardList.map((item) => (item.id === dashboard.id ? dashboard : item)),
});
get().setSettingDashboard(undefined);
const currentDashboard = get().currentDashboard;
set(resolveDashboardMutationState(currentDashboard, get().dashboardList, dashboard));
if (isDashboardMutationCurrent(currentDashboard, dashboard)) {
get().setSettingDashboard(undefined);
}
});
},
deleteDashboard: async (id) => {
Expand All @@ -109,16 +112,16 @@ export const createCommonAction: StateCreator<DashboardStore, [['zustand/devtool
get().updateDashboard({ ...currentDashboard, schema: JSON.stringify(layout) });
},
deleteChart: (id) => {
const mutation = captureDashboardChartDeleteMutation(get().currentDashboard, id);
return deleteChart({ id }).then(() => {
if (!get().currentDashboard?.id) return;
const newChartIds = get().currentDashboard?.chartIds?.filter((t) => t !== id);
const newDashboardDetail: any = {
...get().currentDashboard,
chartIds: newChartIds,
schema: filterSchemaByChartIds(newChartIds, get().currentDashboard?.schema),
};
set({ currentDashboard: newDashboardDetail });
get().updateDashboard(newDashboardDetail);
const updatedDashboard = resolveDashboardChartDeleteTarget(
mutation,
get().currentDashboard,
filterSchemaByChartIds,
);
if (!updatedDashboard) return;
set(resolveDashboardMutationState(get().currentDashboard, get().dashboardList, updatedDashboard));
get().updateDashboard(updatedDashboard);
});
},
getDashboardById: (id) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import assert from 'node:assert/strict';
import { filterSchemaByChartIds } from '@/utils/dashboard';
import {
captureDashboardChartDeleteMutation,
isDashboardMutationCurrent,
resolveDashboardChartDeleteTarget,
resolveDashboardMutationState,
} from './dashboardMutation';

const dashboardA = {
id: 1,
name: 'Dashboard A',
description: 'original A',
chartIds: [10, 20],
schema: JSON.stringify([
{ i: '10', x: 0, y: 0, w: 6, h: 4 },
{ i: '20', x: 6, y: 0, w: 6, h: 4 },
]),
};
const dashboardB = { id: 2, name: 'Dashboard B', description: 'active B', chartIds: [30], schema: '[]' };

const delayedUpdateState = resolveDashboardMutationState(
dashboardB,
[dashboardA, dashboardB],
{ ...dashboardA, name: 'Dashboard A saved' },
);
assert.deepEqual(
delayedUpdateState.currentDashboard,
dashboardB,
'a delayed update for A must not replace the newly selected dashboard B',
);
assert.equal(delayedUpdateState.dashboardList[0].name, 'Dashboard A saved');
assert.equal(isDashboardMutationCurrent(dashboardB, dashboardA), false);
assert.equal(isDashboardMutationCurrent(dashboardA, dashboardA), true);

const currentUpdateState = resolveDashboardMutationState(
{ ...dashboardA, description: 'newer local description' },
[dashboardA, dashboardB],
{ id: 1, name: 'Dashboard A saved' },
);
assert.deepEqual(currentUpdateState.currentDashboard, {
...dashboardA,
name: 'Dashboard A saved',
description: 'newer local description',
});

const deleteMutation = captureDashboardChartDeleteMutation(dashboardA, 10);
const deleteTargetAfterSwitch = resolveDashboardChartDeleteTarget(
deleteMutation,
dashboardB,
filterSchemaByChartIds,
);
assert.deepEqual(deleteTargetAfterSwitch?.chartIds, [20]);
assert.deepEqual(JSON.parse(deleteTargetAfterSwitch?.schema || '[]'), [
{ i: '20', x: 6, y: 0, w: 6, h: 4 },
]);

const deleteStateAfterSwitch = resolveDashboardMutationState(
dashboardB,
[dashboardA, dashboardB],
deleteTargetAfterSwitch!,
);
assert.deepEqual(
deleteStateAfterSwitch.currentDashboard,
dashboardB,
'a delayed chart deletion for A must not remove charts from dashboard B',
);
assert.deepEqual(deleteStateAfterSwitch.dashboardList[0].chartIds, [20]);

const currentAWithNewerName = { ...dashboardA, name: 'Dashboard A renamed while deleting' };
const deleteTargetWithoutSwitch = resolveDashboardChartDeleteTarget(
deleteMutation,
currentAWithNewerName,
filterSchemaByChartIds,
);
assert.equal(deleteTargetWithoutSwitch?.name, 'Dashboard A renamed while deleting');
assert.deepEqual(deleteTargetWithoutSwitch?.chartIds, [20]);

assert.equal(captureDashboardChartDeleteMutation(null, 10), null);
assert.equal(resolveDashboardChartDeleteTarget(null, dashboardB, filterSchemaByChartIds), null);

const listWithoutMutationId = [dashboardA, dashboardB];
assert.equal(
resolveDashboardMutationState(dashboardB, listWithoutMutationId, { name: 'unsaved' }).dashboardList,
listWithoutMutationId,
);

console.log('Dashboard mutation ownership tests passed');
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type { IDashboardItem } from '@/typings';

type DashboardSchemaFilter = (chartIds?: number[], schema?: string) => string;

export interface DashboardChartDeleteMutation {
chartId: number;
dashboardId: number;
dashboardSnapshot: IDashboardItem;
}

export function isDashboardMutationCurrent(
currentDashboard: IDashboardItem | null | undefined,
updatedDashboard: IDashboardItem,
) {
return updatedDashboard.id != null && currentDashboard?.id === updatedDashboard.id;
}

export function resolveDashboardMutationState(
currentDashboard: IDashboardItem | null | undefined,
dashboardList: IDashboardItem[],
updatedDashboard: IDashboardItem,
) {
const ownsCurrentDashboard = isDashboardMutationCurrent(currentDashboard, updatedDashboard);
const updatedDashboardId = updatedDashboard.id;

return {
currentDashboard: ownsCurrentDashboard
? { ...currentDashboard, ...updatedDashboard }
: currentDashboard,
dashboardList:
updatedDashboardId == null
? dashboardList
: dashboardList.map((item) =>
item.id === updatedDashboardId ? { ...item, ...updatedDashboard } : item,
),
};
}

export function captureDashboardChartDeleteMutation(
currentDashboard: IDashboardItem | null | undefined,
chartId: number,
): DashboardChartDeleteMutation | null {
if (currentDashboard?.id == null) {
return null;
}
return {
chartId,
dashboardId: currentDashboard.id,
dashboardSnapshot: {
...currentDashboard,
chartIds: currentDashboard.chartIds ? [...currentDashboard.chartIds] : currentDashboard.chartIds,
},
};
}

export function resolveDashboardChartDeleteTarget(
mutation: DashboardChartDeleteMutation | null,
currentDashboard: IDashboardItem | null | undefined,
filterSchema: DashboardSchemaFilter,
): IDashboardItem | null {
if (!mutation) {
return null;
}
const targetDashboard =
currentDashboard?.id === mutation.dashboardId
? currentDashboard
: mutation.dashboardSnapshot;
const chartIds = targetDashboard.chartIds?.filter((chartId) => chartId !== mutation.chartId);
return {
...targetDashboard,
chartIds,
schema: filterSchema(chartIds, targetDashboard.schema),
};
}
Loading