mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
feat(editor): Show data table storage limit banners (no-changelog) (#19175)
Co-authored-by: Ricardo Espinoza <ricardo@n8n.io> Co-authored-by: Charlie Kolb <charlie@n8n.io>
This commit is contained in:
@@ -56,6 +56,8 @@ describe('DismissBannerRequestDto', () => {
|
|||||||
'TRIAL',
|
'TRIAL',
|
||||||
'NON_PRODUCTION_LICENSE',
|
'NON_PRODUCTION_LICENSE',
|
||||||
'EMAIL_CONFIRMATION',
|
'EMAIL_CONFIRMATION',
|
||||||
|
'DATA_STORE_STORAGE_LIMIT_WARNING',
|
||||||
|
'DATA_STORE_STORAGE_LIMIT_ERROR',
|
||||||
];
|
];
|
||||||
|
|
||||||
expect(bannerNameSchema.options).toEqual(expectedBanners);
|
expect(bannerNameSchema.options).toEqual(expectedBanners);
|
||||||
|
|||||||
@@ -83,7 +83,6 @@ export interface FrontendSettings {
|
|||||||
};
|
};
|
||||||
dataTables: {
|
dataTables: {
|
||||||
maxSize: number;
|
maxSize: number;
|
||||||
warningThreshold: number;
|
|
||||||
};
|
};
|
||||||
personalizationSurveyEnabled: boolean;
|
personalizationSurveyEnabled: boolean;
|
||||||
defaultLocale: string;
|
defaultLocale: string;
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ export const bannerNameSchema = z.enum([
|
|||||||
'TRIAL',
|
'TRIAL',
|
||||||
'NON_PRODUCTION_LICENSE',
|
'NON_PRODUCTION_LICENSE',
|
||||||
'EMAIL_CONFIRMATION',
|
'EMAIL_CONFIRMATION',
|
||||||
|
'DATA_STORE_STORAGE_LIMIT_WARNING',
|
||||||
|
'DATA_STORE_STORAGE_LIMIT_ERROR',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type BannerName = z.infer<typeof bannerNameSchema>;
|
export type BannerName = z.infer<typeof bannerNameSchema>;
|
||||||
|
|||||||
@@ -23,10 +23,7 @@ export class DataStoreSizeValidator {
|
|||||||
return sizeInBytes === undefined;
|
return sizeInBytes === undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getCachedSize(
|
async getCachedSize(fetchSizeFn: () => Promise<number>, now = new Date()): Promise<number> {
|
||||||
fetchSizeFn: () => Promise<number>,
|
|
||||||
now = new Date(),
|
|
||||||
): Promise<number> {
|
|
||||||
// If there's a pending check, wait for it to complete
|
// If there's a pending check, wait for it to complete
|
||||||
|
|
||||||
if (this.pendingCheck) {
|
if (this.pendingCheck) {
|
||||||
|
|||||||
@@ -425,7 +425,9 @@ export class DataStoreService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getDataTablesSize() {
|
async getDataTablesSize() {
|
||||||
const sizeBytes = await this.dataStoreRepository.findDataTablesSize();
|
const sizeBytes = await this.dataStoreSizeValidator.getCachedSize(
|
||||||
|
async () => await this.dataStoreRepository.findDataTablesSize(),
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
sizeBytes,
|
sizeBytes,
|
||||||
sizeState: this.dataStoreSizeValidator.sizeToState(sizeBytes),
|
sizeState: this.dataStoreSizeValidator.sizeToState(sizeBytes),
|
||||||
|
|||||||
@@ -185,7 +185,6 @@ export class FrontendService {
|
|||||||
},
|
},
|
||||||
dataTables: {
|
dataTables: {
|
||||||
maxSize: this.globalConfig.dataTable.maxSize,
|
maxSize: this.globalConfig.dataTable.maxSize,
|
||||||
warningThreshold: this.globalConfig.dataTable.warningThreshold,
|
|
||||||
},
|
},
|
||||||
publicApi: {
|
publicApi: {
|
||||||
enabled: isApiEnabled(),
|
enabled: isApiEnabled(),
|
||||||
|
|||||||
@@ -2901,6 +2901,8 @@
|
|||||||
"dataStore.deleteRows.title": "Delete Rows",
|
"dataStore.deleteRows.title": "Delete Rows",
|
||||||
"dataStore.deleteRows.confirmation": "Are you sure you want to delete {count} row? | Are you sure you want to delete {count} rows?",
|
"dataStore.deleteRows.confirmation": "Are you sure you want to delete {count} row? | Are you sure you want to delete {count} rows?",
|
||||||
"dataStore.deleteRows.error": "Error deleting rows",
|
"dataStore.deleteRows.error": "Error deleting rows",
|
||||||
|
"dataStore.banner.storageLimitWarning.message": "{usage} of Data tables storage used. Delete data to avoid errors",
|
||||||
|
"dataStore.banner.storageLimitError.message": "{usage} of Data tables storage used, operations may fail. Delete data to avoid errors",
|
||||||
"dataStore.error.tableNotInitialized": "Table not initialized",
|
"dataStore.error.tableNotInitialized": "Table not initialized",
|
||||||
"dataStore.noRows": "No rows",
|
"dataStore.noRows": "No rows",
|
||||||
"settings.ldap": "LDAP",
|
"settings.ldap": "LDAP",
|
||||||
|
|||||||
@@ -112,7 +112,6 @@ export const defaultSettings: FrontendSettings = {
|
|||||||
},
|
},
|
||||||
dataTables: {
|
dataTables: {
|
||||||
maxSize: 0,
|
maxSize: 0,
|
||||||
warningThreshold: 0,
|
|
||||||
},
|
},
|
||||||
workflowCallerPolicyDefaultOption: 'any',
|
workflowCallerPolicyDefaultOption: 'any',
|
||||||
workflowTagsDisabled: false,
|
workflowTagsDisabled: false,
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import { useTelemetry } from '@/composables/useTelemetry';
|
|||||||
import { importCurlEventBus, ndvEventBus } from '@/event-bus';
|
import { importCurlEventBus, ndvEventBus } from '@/event-bus';
|
||||||
import { ProjectTypes } from '@/types/projects.types';
|
import { ProjectTypes } from '@/types/projects.types';
|
||||||
import FreeAiCreditsCallout from '@/components/FreeAiCreditsCallout.vue';
|
import FreeAiCreditsCallout from '@/components/FreeAiCreditsCallout.vue';
|
||||||
|
import NodeStorageLimitCallout from '@/features/dataStore/components/NodeStorageLimitCallout.vue';
|
||||||
import { useResizeObserver } from '@vueuse/core';
|
import { useResizeObserver } from '@vueuse/core';
|
||||||
import { useNodeSettingsParameters } from '@/composables/useNodeSettingsParameters';
|
import { useNodeSettingsParameters } from '@/composables/useNodeSettingsParameters';
|
||||||
import { N8nBlockUi, N8nIcon, N8nNotice, N8nText } from '@n8n/design-system';
|
import { N8nBlockUi, N8nIcon, N8nNotice, N8nText } from '@n8n/design-system';
|
||||||
@@ -681,6 +682,7 @@ function handleSelectAction(params: INodeParameters) {
|
|||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<FreeAiCreditsCallout />
|
<FreeAiCreditsCallout />
|
||||||
|
<NodeStorageLimitCallout />
|
||||||
<NodeActionsList
|
<NodeActionsList
|
||||||
v-if="openPanel === 'action'"
|
v-if="openPanel === 'action'"
|
||||||
class="action-tab"
|
class="action-tab"
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ describe('ProjectHeader', () => {
|
|||||||
|
|
||||||
projectsStore.teamProjectsLimit = -1;
|
projectsStore.teamProjectsLimit = -1;
|
||||||
settingsStore.settings.folders = { enabled: false };
|
settingsStore.settings.folders = { enabled: false };
|
||||||
settingsStore.isDataStoreFeatureEnabled = true;
|
settingsStore.isDataTableFeatureEnabled = true;
|
||||||
|
|
||||||
// Setup default moduleTabs structure
|
// Setup default moduleTabs structure
|
||||||
uiStore.moduleTabs = {
|
uiStore.moduleTabs = {
|
||||||
@@ -123,7 +123,7 @@ describe('ProjectHeader', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Overview: should render the correct title and subtitle', async () => {
|
it('Overview: should render the correct title and subtitle', async () => {
|
||||||
settingsStore.isDataStoreFeatureEnabled = false;
|
settingsStore.isDataTableFeatureEnabled = false;
|
||||||
vi.spyOn(projectPages, 'isOverviewSubPage', 'get').mockReturnValue(true);
|
vi.spyOn(projectPages, 'isOverviewSubPage', 'get').mockReturnValue(true);
|
||||||
const { getByTestId, rerender } = renderComponent();
|
const { getByTestId, rerender } = renderComponent();
|
||||||
const overviewSubtitle = 'All the workflows, credentials and executions you have access to';
|
const overviewSubtitle = 'All the workflows, credentials and executions you have access to';
|
||||||
@@ -147,7 +147,7 @@ describe('ProjectHeader', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Personal: should render the correct title and subtitle', async () => {
|
it('Personal: should render the correct title and subtitle', async () => {
|
||||||
settingsStore.isDataStoreFeatureEnabled = false;
|
settingsStore.isDataTableFeatureEnabled = false;
|
||||||
vi.spyOn(projectPages, 'isOverviewSubPage', 'get').mockReturnValue(false);
|
vi.spyOn(projectPages, 'isOverviewSubPage', 'get').mockReturnValue(false);
|
||||||
vi.spyOn(projectPages, 'isSharedSubPage', 'get').mockReturnValue(false);
|
vi.spyOn(projectPages, 'isSharedSubPage', 'get').mockReturnValue(false);
|
||||||
const { getByTestId, rerender } = renderComponent();
|
const { getByTestId, rerender } = renderComponent();
|
||||||
@@ -468,7 +468,7 @@ describe('ProjectHeader', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not render datastore menu item if data store feature is disabled', () => {
|
it('should not render datastore menu item if data store feature is disabled', () => {
|
||||||
settingsStore.isDataStoreFeatureEnabled = false;
|
settingsStore.isDataTableFeatureEnabled = false;
|
||||||
const { getByTestId } = renderComponent();
|
const { getByTestId } = renderComponent();
|
||||||
const actionsContainer = getByTestId('add-resource-actions');
|
const actionsContainer = getByTestId('add-resource-actions');
|
||||||
expect(actionsContainer).toBeInTheDocument();
|
expect(actionsContainer).toBeInTheDocument();
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ const menu = computed(() => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settingsStore.isDataStoreFeatureEnabled) {
|
if (settingsStore.isDataTableFeatureEnabled) {
|
||||||
// TODO: this should probably be moved to the module descriptor as a setting
|
// TODO: this should probably be moved to the module descriptor as a setting
|
||||||
items.push({
|
items.push({
|
||||||
value: ACTION_TYPES.DATA_STORE,
|
value: ACTION_TYPES.DATA_STORE,
|
||||||
@@ -242,13 +242,13 @@ const sectionDescription = computed(() => {
|
|||||||
return i18n.baseText('projects.header.shared.subtitle');
|
return i18n.baseText('projects.header.shared.subtitle');
|
||||||
} else if (projectPages.isOverviewSubPage) {
|
} else if (projectPages.isOverviewSubPage) {
|
||||||
return i18n.baseText(
|
return i18n.baseText(
|
||||||
settingsStore.isDataStoreFeatureEnabled
|
settingsStore.isDataTableFeatureEnabled
|
||||||
? 'projects.header.overview.subtitleWithDataTables'
|
? 'projects.header.overview.subtitleWithDataTables'
|
||||||
: 'projects.header.overview.subtitle',
|
: 'projects.header.overview.subtitle',
|
||||||
);
|
);
|
||||||
} else if (isPersonalProject.value) {
|
} else if (isPersonalProject.value) {
|
||||||
return i18n.baseText(
|
return i18n.baseText(
|
||||||
settingsStore.isDataStoreFeatureEnabled
|
settingsStore.isDataTableFeatureEnabled
|
||||||
? 'projects.header.personal.subtitleWithDataTables'
|
? 'projects.header.personal.subtitleWithDataTables'
|
||||||
: 'projects.header.personal.subtitle',
|
: 'projects.header.personal.subtitle',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import TrialOverBanner from '@/components/banners/TrialOverBanner.vue';
|
|||||||
import TrialBanner from '@/components/banners/TrialBanner.vue';
|
import TrialBanner from '@/components/banners/TrialBanner.vue';
|
||||||
import V1Banner from '@/components/banners/V1Banner.vue';
|
import V1Banner from '@/components/banners/V1Banner.vue';
|
||||||
import EmailConfirmationBanner from '@/components/banners/EmailConfirmationBanner.vue';
|
import EmailConfirmationBanner from '@/components/banners/EmailConfirmationBanner.vue';
|
||||||
|
import DataStoreStorageLimitWarningBanner from '@/components/banners/DataStoreStorageLimitWarningBanner.vue';
|
||||||
|
import DataStoreStorageLimitErrorBanner from '@/components/banners/DataStoreStorageLimitErrorBanner.vue';
|
||||||
import type { Component } from 'vue';
|
import type { Component } from 'vue';
|
||||||
import type { N8nBanners } from '@/Interface';
|
import type { N8nBanners } from '@/Interface';
|
||||||
|
|
||||||
@@ -17,6 +19,14 @@ export const N8N_BANNERS: N8nBanners = {
|
|||||||
EMAIL_CONFIRMATION: { priority: 250, component: EmailConfirmationBanner as Component },
|
EMAIL_CONFIRMATION: { priority: 250, component: EmailConfirmationBanner as Component },
|
||||||
TRIAL: { priority: 150, component: TrialBanner as Component },
|
TRIAL: { priority: 150, component: TrialBanner as Component },
|
||||||
NON_PRODUCTION_LICENSE: { priority: 140, component: NonProductionLicenseBanner as Component },
|
NON_PRODUCTION_LICENSE: { priority: 140, component: NonProductionLicenseBanner as Component },
|
||||||
|
DATA_STORE_STORAGE_LIMIT_WARNING: {
|
||||||
|
priority: 300,
|
||||||
|
component: DataStoreStorageLimitWarningBanner as Component,
|
||||||
|
},
|
||||||
|
DATA_STORE_STORAGE_LIMIT_ERROR: {
|
||||||
|
priority: 400,
|
||||||
|
component: DataStoreStorageLimitErrorBanner as Component,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { useI18n } from '@n8n/i18n';
|
||||||
|
import BaseBanner from '@/components/banners/BaseBanner.vue';
|
||||||
|
import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
|
||||||
|
|
||||||
|
const dataStoreStore = useDataStoreStore();
|
||||||
|
const i18n = useI18n();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<BaseBanner name="DATA_STORE_STORAGE_LIMIT_ERROR" :dismissible="true" theme="danger">
|
||||||
|
<template #mainContent>
|
||||||
|
<span>{{
|
||||||
|
i18n.baseText('dataStore.banner.storageLimitError.message', {
|
||||||
|
interpolate: {
|
||||||
|
usage: `${dataStoreStore.dataStoreSize} / ${dataStoreStore.maxSizeMB}MB`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}}</span>
|
||||||
|
</template>
|
||||||
|
</BaseBanner>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { useI18n } from '@n8n/i18n';
|
||||||
|
import BaseBanner from '@/components/banners/BaseBanner.vue';
|
||||||
|
import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
|
||||||
|
|
||||||
|
const dataStoreStore = useDataStoreStore();
|
||||||
|
const i18n = useI18n();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<BaseBanner name="DATA_STORE_STORAGE_LIMIT_WARNING" :dismissible="true" theme="warning">
|
||||||
|
<template #mainContent>
|
||||||
|
<span>{{
|
||||||
|
i18n.baseText('dataStore.banner.storageLimitWarning.message', {
|
||||||
|
interpolate: {
|
||||||
|
usage: `${dataStoreStore.dataStoreSize} / ${dataStoreStore.maxSizeMB}MB`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}}</span>
|
||||||
|
</template>
|
||||||
|
</BaseBanner>
|
||||||
|
</template>
|
||||||
@@ -235,6 +235,8 @@ export const CREDENTIAL_ONLY_HTTP_NODE_VERSION = 4.1;
|
|||||||
// template categories
|
// template categories
|
||||||
export const TEMPLATE_CATEGORY_AI = 'categories/ai';
|
export const TEMPLATE_CATEGORY_AI = 'categories/ai';
|
||||||
|
|
||||||
|
export const DATA_STORE_NODES = [DATA_STORE_NODE_TYPE, DATA_STORE_TOOL_NODE_TYPE];
|
||||||
|
|
||||||
export const EXECUTABLE_TRIGGER_NODE_TYPES = [
|
export const EXECUTABLE_TRIGGER_NODE_TYPES = [
|
||||||
START_NODE_TYPE,
|
START_NODE_TYPE,
|
||||||
MANUAL_TRIGGER_NODE_TYPE,
|
MANUAL_TRIGGER_NODE_TYPE,
|
||||||
@@ -256,8 +258,7 @@ export const NODES_USING_CODE_NODE_EDITOR = [
|
|||||||
AI_TRANSFORM_NODE_TYPE,
|
AI_TRANSFORM_NODE_TYPE,
|
||||||
];
|
];
|
||||||
export const MODULE_ENABLED_NODES = [
|
export const MODULE_ENABLED_NODES = [
|
||||||
{ nodeType: DATA_STORE_NODE_TYPE, module: DATA_STORE_MODULE_NAME },
|
...DATA_STORE_NODES.map((nodeType) => ({ nodeType, module: DATA_STORE_MODULE_NAME })),
|
||||||
{ nodeType: DATA_STORE_TOOL_NODE_TYPE, module: DATA_STORE_MODULE_NAME },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export const NODE_POSITION_CONFLICT_ALLOWLIST = [STICKY_NODE_TYPE];
|
export const NODE_POSITION_CONFLICT_ALLOWLIST = [STICKY_NODE_TYPE];
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useI18n } from '@n8n/i18n';
|
||||||
|
import { N8nCallout } from '@n8n/design-system';
|
||||||
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
|
import { DATA_STORE_NODES } from '@/constants';
|
||||||
|
import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
|
||||||
|
|
||||||
|
const i18n = useI18n();
|
||||||
|
const nvdStore = useNDVStore();
|
||||||
|
const dataStoreStore = useDataStoreStore();
|
||||||
|
|
||||||
|
const calloutType = computed(() => {
|
||||||
|
if (!DATA_STORE_NODES.includes(nvdStore.activeNode?.type ?? '')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sizeLimitState = dataStoreStore.dataStoreSizeLimitState;
|
||||||
|
switch (sizeLimitState) {
|
||||||
|
case 'error':
|
||||||
|
return 'danger';
|
||||||
|
case 'warn':
|
||||||
|
return 'warning';
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<N8nCallout v-if="calloutType" :theme="calloutType" class="mt-xs">
|
||||||
|
<span v-if="calloutType === 'danger'">
|
||||||
|
{{
|
||||||
|
i18n.baseText('dataStore.banner.storageLimitError.message', {
|
||||||
|
interpolate: {
|
||||||
|
usage: `${dataStoreStore.dataStoreSize} / ${dataStoreStore.maxSizeMB}MB`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
{{
|
||||||
|
i18n.baseText('dataStore.banner.storageLimitWarning.message', {
|
||||||
|
interpolate: {
|
||||||
|
usage: `${dataStoreStore.dataStoreSize} / ${dataStoreStore.maxSizeMB}MB`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</N8nCallout>
|
||||||
|
</template>
|
||||||
@@ -7,6 +7,7 @@ import type {
|
|||||||
DataStoreColumn,
|
DataStoreColumn,
|
||||||
DataStoreRow,
|
DataStoreRow,
|
||||||
} from '@/features/dataStore/datastore.types';
|
} from '@/features/dataStore/datastore.types';
|
||||||
|
import type { DataTablesSizeResult } from 'n8n-workflow';
|
||||||
|
|
||||||
export const fetchDataStoresApi = async (
|
export const fetchDataStoresApi = async (
|
||||||
context: IRestApiContext,
|
context: IRestApiContext,
|
||||||
@@ -203,3 +204,11 @@ export const deleteDataStoreRowsApi = async (
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const fetchDataStoreGlobalLimitInBytes = async (context: IRestApiContext) => {
|
||||||
|
return await makeRestApiRequest<DataTablesSizeResult>(
|
||||||
|
context,
|
||||||
|
'GET',
|
||||||
|
'/data-tables-global/limits',
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { DATA_STORE_STORE } from '@/features/dataStore/constants';
|
import { DATA_STORE_STORE } from '@/features/dataStore/constants';
|
||||||
import { ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { useRootStore } from '@n8n/stores/useRootStore';
|
import { useRootStore } from '@n8n/stores/useRootStore';
|
||||||
import {
|
import {
|
||||||
fetchDataStoresApi,
|
fetchDataStoresApi,
|
||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
insertDataStoreRowApi,
|
insertDataStoreRowApi,
|
||||||
updateDataStoreRowsApi,
|
updateDataStoreRowsApi,
|
||||||
deleteDataStoreRowsApi,
|
deleteDataStoreRowsApi,
|
||||||
|
fetchDataStoreGlobalLimitInBytes,
|
||||||
} from '@/features/dataStore/dataStore.api';
|
} from '@/features/dataStore/dataStore.api';
|
||||||
import type {
|
import type {
|
||||||
DataStore,
|
DataStore,
|
||||||
@@ -22,13 +23,22 @@ import type {
|
|||||||
} from '@/features/dataStore/datastore.types';
|
} from '@/features/dataStore/datastore.types';
|
||||||
import { useProjectsStore } from '@/stores/projects.store';
|
import { useProjectsStore } from '@/stores/projects.store';
|
||||||
import { reorderItem } from '@/features/dataStore/utils';
|
import { reorderItem } from '@/features/dataStore/utils';
|
||||||
|
import { type DataTableSizeStatus } from 'n8n-workflow';
|
||||||
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
|
|
||||||
export const useDataStoreStore = defineStore(DATA_STORE_STORE, () => {
|
export const useDataStoreStore = defineStore(DATA_STORE_STORE, () => {
|
||||||
const rootStore = useRootStore();
|
const rootStore = useRootStore();
|
||||||
const projectStore = useProjectsStore();
|
const projectStore = useProjectsStore();
|
||||||
|
const settingsStore = useSettingsStore();
|
||||||
|
|
||||||
const dataStores = ref<DataStore[]>([]);
|
const dataStores = ref<DataStore[]>([]);
|
||||||
const totalCount = ref(0);
|
const totalCount = ref(0);
|
||||||
|
const dataStoreSize = ref(0);
|
||||||
|
const dataStoreSizeLimitState = ref<DataTableSizeStatus>('ok');
|
||||||
|
|
||||||
|
const maxSizeMB = computed(() =>
|
||||||
|
Math.floor(settingsStore.settings?.dataTables?.maxSize / 1024 / 1024),
|
||||||
|
);
|
||||||
|
|
||||||
const fetchDataStores = async (projectId: string, page: number, pageSize: number) => {
|
const fetchDataStores = async (projectId: string, page: number, pageSize: number) => {
|
||||||
const response = await fetchDataStoresApi(rootStore.restApiContext, projectId, {
|
const response = await fetchDataStoresApi(rootStore.restApiContext, projectId, {
|
||||||
@@ -207,10 +217,21 @@ export const useDataStoreStore = defineStore(DATA_STORE_STORE, () => {
|
|||||||
return await deleteDataStoreRowsApi(rootStore.restApiContext, dataStoreId, rowIds, projectId);
|
return await deleteDataStoreRowsApi(rootStore.restApiContext, dataStoreId, rowIds, projectId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchDataStoreSize = async () => {
|
||||||
|
const result = await fetchDataStoreGlobalLimitInBytes(rootStore.restApiContext);
|
||||||
|
dataStoreSize.value = Number((result.sizeBytes / 1024 / 1024).toFixed(2));
|
||||||
|
dataStoreSizeLimitState.value = result.sizeState;
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dataStores,
|
dataStores,
|
||||||
totalCount,
|
totalCount,
|
||||||
fetchDataStores,
|
fetchDataStores,
|
||||||
|
fetchDataStoreSize,
|
||||||
|
dataStoreSize: computed(() => dataStoreSize.value),
|
||||||
|
dataStoreSizeLimitState: computed(() => dataStoreSizeLimitState.value),
|
||||||
|
maxSizeMB,
|
||||||
createDataStore,
|
createDataStore,
|
||||||
deleteDataStore,
|
deleteDataStore,
|
||||||
updateDataStore,
|
updateDataStore,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { useI18n } from '@n8n/i18n';
|
|||||||
import { useRootStore } from '@n8n/stores/useRootStore';
|
import { useRootStore } from '@n8n/stores/useRootStore';
|
||||||
import { h } from 'vue';
|
import { h } from 'vue';
|
||||||
import { useRolesStore } from './stores/roles.store';
|
import { useRolesStore } from './stores/roles.store';
|
||||||
|
import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
|
||||||
|
|
||||||
export const state = {
|
export const state = {
|
||||||
initialized: false,
|
initialized: false,
|
||||||
@@ -134,6 +135,7 @@ export async function initializeAuthenticatedFeatures(
|
|||||||
const insightsStore = useInsightsStore();
|
const insightsStore = useInsightsStore();
|
||||||
const uiStore = useUIStore();
|
const uiStore = useUIStore();
|
||||||
const versionsStore = useVersionsStore();
|
const versionsStore = useVersionsStore();
|
||||||
|
const dataStoreStore = useDataStoreStore();
|
||||||
|
|
||||||
if (sourceControlStore.isEnterpriseSourceControlEnabled) {
|
if (sourceControlStore.isEnterpriseSourceControlEnabled) {
|
||||||
try {
|
try {
|
||||||
@@ -172,6 +174,15 @@ export async function initializeAuthenticatedFeatures(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (settingsStore.isDataTableFeatureEnabled) {
|
||||||
|
const { sizeState } = await dataStoreStore.fetchDataStoreSize();
|
||||||
|
if (sizeState === 'error') {
|
||||||
|
uiStore.pushBannerToStack('DATA_STORE_STORAGE_LIMIT_ERROR');
|
||||||
|
} else if (sizeState === 'warn') {
|
||||||
|
uiStore.pushBannerToStack('DATA_STORE_STORAGE_LIMIT_WARNING');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (insightsStore.isSummaryEnabled) {
|
if (insightsStore.isSummaryEnabled) {
|
||||||
void insightsStore.weeklySummary.execute();
|
void insightsStore.weeklySummary.execute();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
|
|||||||
|
|
||||||
const isFoldersFeatureEnabled = computed(() => folders.value.enabled);
|
const isFoldersFeatureEnabled = computed(() => folders.value.enabled);
|
||||||
|
|
||||||
const isDataStoreFeatureEnabled = computed(() => isModuleActive('data-table'));
|
const isDataTableFeatureEnabled = computed(() => isModuleActive('data-table'));
|
||||||
|
|
||||||
const areTagsEnabled = computed(() =>
|
const areTagsEnabled = computed(() =>
|
||||||
settings.value.workflowTagsDisabled !== undefined ? !settings.value.workflowTagsDisabled : true,
|
settings.value.workflowTagsDisabled !== undefined ? !settings.value.workflowTagsDisabled : true,
|
||||||
@@ -186,6 +186,7 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
|
|||||||
|
|
||||||
const setSettings = (newSettings: FrontendSettings) => {
|
const setSettings = (newSettings: FrontendSettings) => {
|
||||||
settings.value = newSettings;
|
settings.value = newSettings;
|
||||||
|
|
||||||
userManagement.value = newSettings.userManagement;
|
userManagement.value = newSettings.userManagement;
|
||||||
if (userManagement.value) {
|
if (userManagement.value) {
|
||||||
userManagement.value.showSetupOnFirstLoad =
|
userManagement.value.showSetupOnFirstLoad =
|
||||||
@@ -386,6 +387,6 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
|
|||||||
isMFAEnforced,
|
isMFAEnforced,
|
||||||
activeModules,
|
activeModules,
|
||||||
isModuleActive,
|
isModuleActive,
|
||||||
isDataStoreFeatureEnabled,
|
isDataTableFeatureEnabled,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user