mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(editor): Add time range selector to Insights (#14877)
Co-authored-by: Guillaume Jacquart <jacquart.guillaume@gmail.com>
This commit is contained in:
committed by
GitHub
parent
2d60e469f3
commit
bfd85dd3c9
@@ -1,5 +1,7 @@
|
|||||||
import type { ExpressionEvaluatorType, LogLevel, WorkflowSettings } from 'n8n-workflow';
|
import type { ExpressionEvaluatorType, LogLevel, WorkflowSettings } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import { type InsightsDateRange } from './schemas/insights.schema';
|
||||||
|
|
||||||
export interface IVersionNotificationSettings {
|
export interface IVersionNotificationSettings {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
@@ -193,5 +195,6 @@ export interface FrontendSettings {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
summary: boolean;
|
summary: boolean;
|
||||||
dashboard: boolean;
|
dashboard: boolean;
|
||||||
|
dateRanges: InsightsDateRange[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -251,6 +251,7 @@ export class FrontendService {
|
|||||||
enabled: this.modulesConfig.modules.includes('insights'),
|
enabled: this.modulesConfig.modules.includes('insights'),
|
||||||
summary: true,
|
summary: true,
|
||||||
dashboard: false,
|
dashboard: false,
|
||||||
|
dateRanges: [],
|
||||||
},
|
},
|
||||||
logsView: {
|
logsView: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
|||||||
@@ -149,6 +149,15 @@ export const defaultSettings: FrontendSettings = {
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
summary: true,
|
summary: true,
|
||||||
dashboard: false,
|
dashboard: false,
|
||||||
|
dateRanges: [
|
||||||
|
{ key: 'day', licensed: true, granularity: 'hour' },
|
||||||
|
{ key: 'week', licensed: true, granularity: 'day' },
|
||||||
|
{ key: '2weeks', licensed: true, granularity: 'day' },
|
||||||
|
{ key: 'month', licensed: false, granularity: 'day' },
|
||||||
|
{ key: 'quarter', licensed: false, granularity: 'week' },
|
||||||
|
{ key: '6months', licensed: false, granularity: 'week' },
|
||||||
|
{ key: 'year', licensed: false, granularity: 'week' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
logsView: {
|
logsView: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
|
import InsightsDashboard from './InsightsDashboard.vue';
|
||||||
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
|
import { defaultSettings } from '@/__tests__/defaults';
|
||||||
|
import { useInsightsStore } from '@/features/insights/insights.store';
|
||||||
|
import { mockedStore } from '@/__tests__/utils';
|
||||||
|
import { within } from '@testing-library/vue';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
|
||||||
|
const renderComponent = createComponentRenderer(InsightsDashboard, {
|
||||||
|
props: {
|
||||||
|
insightType: 'total',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('InsightsDashboard', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
createTestingPinia({ initialState: { settings: { settings: defaultSettings } } });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render without error', () => {
|
||||||
|
mockedStore(useInsightsStore);
|
||||||
|
expect(() => renderComponent()).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update the selected time range', async () => {
|
||||||
|
mockedStore(useInsightsStore);
|
||||||
|
|
||||||
|
const { getByTestId } = renderComponent();
|
||||||
|
|
||||||
|
expect(getByTestId('range-select')).toBeVisible();
|
||||||
|
const select = within(getByTestId('range-select')).getByRole('combobox');
|
||||||
|
await userEvent.click(select);
|
||||||
|
|
||||||
|
const controllingId = select.getAttribute('aria-controls');
|
||||||
|
const actions = document.querySelector(`#${controllingId}`);
|
||||||
|
if (!actions) {
|
||||||
|
throw new Error('Actions menu not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
await userEvent.click(actions.querySelectorAll('li')[0]);
|
||||||
|
expect((select as HTMLInputElement).value).toBe('Last 24 hours');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
import InsightsSummary from '@/features/insights/components/InsightsSummary.vue';
|
import InsightsSummary from '@/features/insights/components/InsightsSummary.vue';
|
||||||
import { useInsightsStore } from '@/features/insights/insights.store';
|
import { useInsightsStore } from '@/features/insights/insights.store';
|
||||||
import type { InsightsSummaryType } from '@n8n/api-types';
|
import type { InsightsDateRange, InsightsSummaryType } from '@n8n/api-types';
|
||||||
import { computed, defineAsyncComponent, ref, watch } from 'vue';
|
import { computed, defineAsyncComponent, ref, watch } from 'vue';
|
||||||
|
import { TELEMETRY_TIME_RANGE, UNLICENSED_TIME_RANGE } from '../insights.constants';
|
||||||
|
import InsightsDateRangeSelect from './InsightsDateRangeSelect.vue';
|
||||||
|
import InsightsUpgradeModal from './InsightsUpgradeModal.vue';
|
||||||
|
|
||||||
const InsightsPaywall = defineAsyncComponent(
|
const InsightsPaywall = defineAsyncComponent(
|
||||||
async () => await import('@/features/insights/components/InsightsPaywall.vue'),
|
async () => await import('@/features/insights/components/InsightsPaywall.vue'),
|
||||||
@@ -32,6 +36,7 @@ const props = defineProps<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
const telemetry = useTelemetry();
|
||||||
|
|
||||||
const insightsStore = useInsightsStore();
|
const insightsStore = useInsightsStore();
|
||||||
|
|
||||||
@@ -53,10 +58,12 @@ const fetchPaginatedTableData = ({
|
|||||||
page = 0,
|
page = 0,
|
||||||
itemsPerPage = 20,
|
itemsPerPage = 20,
|
||||||
sortBy,
|
sortBy,
|
||||||
|
dateRange = selectedDateRange.value,
|
||||||
}: {
|
}: {
|
||||||
page?: number;
|
page?: number;
|
||||||
itemsPerPage?: number;
|
itemsPerPage?: number;
|
||||||
sortBy: Array<{ id: string; desc: boolean }>;
|
sortBy: Array<{ id: string; desc: boolean }>;
|
||||||
|
dateRange?: InsightsDateRange['key'];
|
||||||
}) => {
|
}) => {
|
||||||
const skip = page * itemsPerPage;
|
const skip = page * itemsPerPage;
|
||||||
const take = itemsPerPage;
|
const take = itemsPerPage;
|
||||||
@@ -67,22 +74,42 @@ const fetchPaginatedTableData = ({
|
|||||||
skip,
|
skip,
|
||||||
take,
|
take,
|
||||||
sortBy: sortKey,
|
sortBy: sortKey,
|
||||||
|
dateRange,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const sortTableBy = ref([{ id: props.insightType, desc: true }]);
|
const sortTableBy = ref([{ id: props.insightType, desc: true }]);
|
||||||
|
|
||||||
|
const upgradeModalVisible = ref(false);
|
||||||
|
const selectedDateRange = ref<InsightsDateRange['key']>('week');
|
||||||
|
const granularity = computed(
|
||||||
|
() =>
|
||||||
|
insightsStore.dateRanges.find((item) => item.key === selectedDateRange.value)?.granularity ??
|
||||||
|
'day',
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleTimeChange(value: InsightsDateRange['key'] | typeof UNLICENSED_TIME_RANGE) {
|
||||||
|
if (value === UNLICENSED_TIME_RANGE) {
|
||||||
|
upgradeModalVisible.value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedDateRange.value = value;
|
||||||
|
telemetry.track('User updated insights time range', { range: TELEMETRY_TIME_RANGE[value] });
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.insightType,
|
() => [props.insightType, selectedDateRange.value],
|
||||||
() => {
|
() => {
|
||||||
sortTableBy.value = [{ id: props.insightType, desc: true }];
|
sortTableBy.value = [{ id: props.insightType, desc: true }];
|
||||||
|
|
||||||
if (insightsStore.isSummaryEnabled) {
|
if (insightsStore.isSummaryEnabled) {
|
||||||
void insightsStore.summary.execute();
|
void insightsStore.summary.execute(0, { dateRange: selectedDateRange.value });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (insightsStore.isDashboardEnabled) {
|
if (insightsStore.isDashboardEnabled) {
|
||||||
void insightsStore.charts.execute();
|
void insightsStore.charts.execute(0, { dateRange: selectedDateRange.value });
|
||||||
fetchPaginatedTableData({ sortBy: sortTableBy.value });
|
fetchPaginatedTableData({ sortBy: sortTableBy.value, dateRange: selectedDateRange.value });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -97,51 +124,63 @@ watch(
|
|||||||
<N8nHeading bold tag="h2" size="xlarge">
|
<N8nHeading bold tag="h2" size="xlarge">
|
||||||
{{ i18n.baseText('insights.dashboard.title') }}
|
{{ i18n.baseText('insights.dashboard.title') }}
|
||||||
</N8nHeading>
|
</N8nHeading>
|
||||||
<div>
|
|
||||||
<InsightsSummary
|
<div class="mt-s">
|
||||||
v-if="insightsStore.isSummaryEnabled"
|
<InsightsDateRangeSelect
|
||||||
:summary="insightsStore.summary.state"
|
:model-value="selectedDateRange"
|
||||||
:loading="insightsStore.summary.isLoading"
|
style="width: 173px"
|
||||||
:class="$style.insightsBanner"
|
data-test-id="range-select"
|
||||||
|
@update:model-value="handleTimeChange"
|
||||||
/>
|
/>
|
||||||
<div :class="$style.insightsContent">
|
|
||||||
<InsightsPaywall
|
<InsightsUpgradeModal v-model="upgradeModalVisible" />
|
||||||
v-if="!insightsStore.isDashboardEnabled"
|
</div>
|
||||||
data-test-id="insights-dashboard-unlicensed"
|
|
||||||
/>
|
<InsightsSummary
|
||||||
<div v-else>
|
v-if="insightsStore.isSummaryEnabled"
|
||||||
<div :class="$style.insightsChartWrapper">
|
:summary="insightsStore.summary.state"
|
||||||
<div v-if="insightsStore.charts.isLoading" :class="$style.chartLoader">
|
:loading="insightsStore.summary.isLoading"
|
||||||
<svg
|
:time-range="selectedDateRange"
|
||||||
width="22"
|
:class="$style.insightsBanner"
|
||||||
height="22"
|
/>
|
||||||
viewBox="0 0 22 22"
|
<div :class="$style.insightsContent">
|
||||||
fill="none"
|
<InsightsPaywall
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
v-if="!insightsStore.isDashboardEnabled"
|
||||||
>
|
data-test-id="insights-dashboard-unlicensed"
|
||||||
<path
|
/>
|
||||||
d="M21 11C21 16.5228 16.5228 21 11 21C5.47715 21 1 16.5228 1 11C1 5.47715 5.47715 1 11 1C11.6293 1 12.245 1.05813 12.8421 1.16931"
|
<div v-else>
|
||||||
stroke="currentColor"
|
<div :class="$style.insightsChartWrapper">
|
||||||
stroke-width="2"
|
<div v-if="insightsStore.charts.isLoading" :class="$style.chartLoader">
|
||||||
/>
|
<svg
|
||||||
</svg>
|
width="22"
|
||||||
{{ i18n.baseText('insights.chart.loading') }}
|
height="22"
|
||||||
</div>
|
viewBox="0 0 22 22"
|
||||||
<component
|
fill="none"
|
||||||
:is="chartComponents[props.insightType]"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
v-else
|
>
|
||||||
:type="props.insightType"
|
<path
|
||||||
:data="insightsStore.charts.state"
|
d="M21 11C21 16.5228 16.5228 21 11 21C5.47715 21 1 16.5228 1 11C1 5.47715 5.47715 1 11 1C11.6293 1 12.245 1.05813 12.8421 1.16931"
|
||||||
/>
|
stroke="currentColor"
|
||||||
</div>
|
stroke-width="2"
|
||||||
<div :class="$style.insightsTableWrapper">
|
/>
|
||||||
<InsightsTableWorkflows
|
</svg>
|
||||||
v-model:sort-by="sortTableBy"
|
{{ i18n.baseText('insights.chart.loading') }}
|
||||||
:data="insightsStore.table.state"
|
|
||||||
:loading="insightsStore.table.isLoading"
|
|
||||||
@update:options="fetchPaginatedTableData"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<component
|
||||||
|
:is="chartComponents[props.insightType]"
|
||||||
|
v-else
|
||||||
|
:type="props.insightType"
|
||||||
|
:data="insightsStore.charts.state"
|
||||||
|
:granularity
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.insightsTableWrapper">
|
||||||
|
<InsightsTableWorkflows
|
||||||
|
v-model:sort-by="sortTableBy"
|
||||||
|
:data="insightsStore.table.state"
|
||||||
|
:loading="insightsStore.table.isLoading"
|
||||||
|
@update:options="fetchPaginatedTableData"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useInsightsStore } from '@/features/insights/insights.store';
|
||||||
|
import type { InsightsDateRange } from '@n8n/api-types';
|
||||||
|
import { N8nOption, N8nSelect } from '@n8n/design-system';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { TIME_RANGE_LABELS, UNLICENSED_TIME_RANGE } from '../insights.constants';
|
||||||
|
|
||||||
|
const model = defineModel<InsightsDateRange['key'] | typeof UNLICENSED_TIME_RANGE>({
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const insightsStore = useInsightsStore();
|
||||||
|
|
||||||
|
const timeOptions = ref(
|
||||||
|
insightsStore.dateRanges.map((option) => {
|
||||||
|
return {
|
||||||
|
key: option.key,
|
||||||
|
label: TIME_RANGE_LABELS[option.key],
|
||||||
|
value: option.licensed ? option.key : UNLICENSED_TIME_RANGE,
|
||||||
|
licensed: option.licensed,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<N8nSelect v-model="model" size="small">
|
||||||
|
<N8nOption v-for="item in timeOptions" :key="item.key" :value="item.value" :label="item.label">
|
||||||
|
{{ item.label }}
|
||||||
|
<svg
|
||||||
|
v-if="!item.licensed"
|
||||||
|
width="16"
|
||||||
|
height="17"
|
||||||
|
viewBox="0 0 16 17"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
style="margin-left: auto"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12.6667 7.83337H3.33333C2.59695 7.83337 2 8.43033 2 9.16671V13.8334C2 14.5698 2.59695 15.1667 3.33333 15.1667H12.6667C13.403 15.1667 14 14.5698 14 13.8334V9.16671C14 8.43033 13.403 7.83337 12.6667 7.83337Z"
|
||||||
|
stroke="#9A9A9A"
|
||||||
|
stroke-width="1.33333"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M4.66681 7.83337V5.16671C4.66681 4.28265 5.018 3.43481 5.64312 2.80968C6.26824 2.18456 7.11609 1.83337 8.00014 1.83337C8.8842 1.83337 9.73204 2.18456 10.3572 2.80968C10.9823 3.43481 11.3335 4.28265 11.3335 5.16671V7.83337"
|
||||||
|
stroke="#9A9A9A"
|
||||||
|
stroke-width="1.33333"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</N8nOption>
|
||||||
|
</N8nSelect>
|
||||||
|
</template>
|
||||||
@@ -27,6 +27,7 @@ describe('InsightsSummary', () => {
|
|||||||
renderComponent({
|
renderComponent({
|
||||||
props: {
|
props: {
|
||||||
summary: [],
|
summary: [],
|
||||||
|
timeRange: 'week',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
).not.toThrow();
|
).not.toThrow();
|
||||||
@@ -95,6 +96,7 @@ describe('InsightsSummary', () => {
|
|||||||
const { html } = renderComponent({
|
const { html } = renderComponent({
|
||||||
props: {
|
props: {
|
||||||
summary,
|
summary,
|
||||||
|
timeRange: 'week',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,15 +5,17 @@ import { VIEWS } from '@/constants';
|
|||||||
import {
|
import {
|
||||||
INSIGHT_IMPACT_TYPES,
|
INSIGHT_IMPACT_TYPES,
|
||||||
INSIGHTS_UNIT_IMPACT_MAPPING,
|
INSIGHTS_UNIT_IMPACT_MAPPING,
|
||||||
|
TIME_RANGE_LABELS,
|
||||||
} from '@/features/insights/insights.constants';
|
} from '@/features/insights/insights.constants';
|
||||||
import type { InsightsSummaryDisplay } from '@/features/insights/insights.types';
|
import type { InsightsSummaryDisplay } from '@/features/insights/insights.types';
|
||||||
import type { InsightsSummary } from '@n8n/api-types';
|
import type { InsightsDateRange, InsightsSummary } from '@n8n/api-types';
|
||||||
import { smartDecimal } from '@n8n/utils/number/smartDecimal';
|
import { smartDecimal } from '@n8n/utils/number/smartDecimal';
|
||||||
import { computed, ref, useCssModule } from 'vue';
|
import { computed, useCssModule } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
summary: InsightsSummaryDisplay;
|
summary: InsightsSummaryDisplay;
|
||||||
|
timeRange: InsightsDateRange['key'];
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
@@ -22,8 +24,6 @@ const route = useRoute();
|
|||||||
const $style = useCssModule();
|
const $style = useCssModule();
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
|
|
||||||
const lastNDays = ref(7);
|
|
||||||
|
|
||||||
const summaryTitles = computed<Record<keyof InsightsSummary, string>>(() => ({
|
const summaryTitles = computed<Record<keyof InsightsSummary, string>>(() => ({
|
||||||
total: i18n.baseText('insights.banner.title.total'),
|
total: i18n.baseText('insights.banner.title.total'),
|
||||||
failed: i18n.baseText('insights.banner.title.failed'),
|
failed: i18n.baseText('insights.banner.title.failed'),
|
||||||
@@ -84,9 +84,9 @@ const trackTabClick = (insightType: keyof InsightsSummary) => {
|
|||||||
{{ summaryTitles[id] }}
|
{{ summaryTitles[id] }}
|
||||||
</N8nTooltip>
|
</N8nTooltip>
|
||||||
</strong>
|
</strong>
|
||||||
<small :class="$style.days">{{
|
<small :class="$style.days">
|
||||||
i18n.baseText('insights.lastNDays', { interpolate: { count: lastNDays } })
|
{{ TIME_RANGE_LABELS[timeRange] }}
|
||||||
}}</small>
|
</small>
|
||||||
<span v-if="summaryHasNoData" :class="$style.noData">
|
<span v-if="summaryHasNoData" :class="$style.noData">
|
||||||
<N8nTooltip placement="bottom">
|
<N8nTooltip placement="bottom">
|
||||||
<template #content>
|
<template #content>
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
|
||||||
|
import type { BaseTextKey } from '@/plugins/i18n';
|
||||||
|
import { N8nButton, N8nText } from '@n8n/design-system';
|
||||||
|
import { ElDialog } from 'element-plus';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const model = defineModel<boolean>();
|
||||||
|
const i18n = useI18n();
|
||||||
|
|
||||||
|
function goToUpgrade() {
|
||||||
|
model.value = false;
|
||||||
|
void usePageRedirectionHelper().goToUpgrade('insights', 'upgrade-insights');
|
||||||
|
}
|
||||||
|
|
||||||
|
const perks = computed(() =>
|
||||||
|
[...Array(3).keys()].map((index) =>
|
||||||
|
i18n.baseText(`insights.upgradeModal.perks.${index}` as BaseTextKey),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ElDialog v-model="model" :title="i18n.baseText('insights.upgradeModal.title')" width="500">
|
||||||
|
<div>
|
||||||
|
<N8nText tag="p" class="mb-s">
|
||||||
|
{{ i18n.baseText('insights.upgradeModal.content') }}
|
||||||
|
</N8nText>
|
||||||
|
<ul class="perks-list">
|
||||||
|
<N8nText v-for="perk in perks" :key="perk" color="text-dark" tag="li">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16px" height="16px">
|
||||||
|
<path
|
||||||
|
d="M 16 8 C 16 12.418 12.418 16 8 16 C 3.582 16 0 12.418 0 8 C 0 3.582 3.582 0 8 0 C 12.418 0 16 3.582 16 8 Z M 3.97 9.03 L 5.97 11.03 L 6.5 11.561 L 7.03 11.03 L 12.53 5.53 L 11.47 4.47 L 6.5 9.439 L 5.03 7.97 L 3.97 9.03 Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{{ perk }}
|
||||||
|
</N8nText>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<div>
|
||||||
|
<N8nButton type="secondary" @click="model = false">
|
||||||
|
{{ i18n.baseText('insights.upgradeModal.button.dismiss') }}
|
||||||
|
</N8nButton>
|
||||||
|
<N8nButton type="primary" @click="goToUpgrade">
|
||||||
|
{{ i18n.baseText('insights.upgradeModal.button.upgrade') }}
|
||||||
|
</N8nButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ElDialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.perks-list {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
|
||||||
|
> li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-2xs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -4,20 +4,18 @@ import {
|
|||||||
generateLinearGradient,
|
generateLinearGradient,
|
||||||
generateLineChartOptions,
|
generateLineChartOptions,
|
||||||
} from '@/features/insights/chartjs.utils';
|
} from '@/features/insights/chartjs.utils';
|
||||||
import { DATE_FORMAT_MASK, INSIGHTS_UNIT_MAPPING } from '@/features/insights/insights.constants';
|
import {
|
||||||
|
GRANULARITY_DATE_FORMAT_MASK,
|
||||||
|
INSIGHTS_UNIT_MAPPING,
|
||||||
|
} from '@/features/insights/insights.constants';
|
||||||
import { transformInsightsAverageRunTime } from '@/features/insights/insights.utils';
|
import { transformInsightsAverageRunTime } from '@/features/insights/insights.utils';
|
||||||
import type { InsightsByTime, InsightsSummaryType } from '@n8n/api-types';
|
|
||||||
import { smartDecimal } from '@n8n/utils/number/smartDecimal';
|
import { smartDecimal } from '@n8n/utils/number/smartDecimal';
|
||||||
import { type ChartData, Filler, type ScriptableContext } from 'chart.js';
|
import { type ChartData, Filler, type ScriptableContext } from 'chart.js';
|
||||||
import dateformat from 'dateformat';
|
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { Line } from 'vue-chartjs';
|
import { Line } from 'vue-chartjs';
|
||||||
|
import type { ChartProps } from './insightChartProps';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<ChartProps>();
|
||||||
data: InsightsByTime[];
|
|
||||||
type: InsightsSummaryType;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
const chartOptions = computed(() =>
|
const chartOptions = computed(() =>
|
||||||
@@ -40,7 +38,7 @@ const chartData = computed<ChartData<'line'>>(() => {
|
|||||||
const data: number[] = [];
|
const data: number[] = [];
|
||||||
|
|
||||||
for (const entry of props.data) {
|
for (const entry of props.data) {
|
||||||
labels.push(dateformat(entry.date, DATE_FORMAT_MASK));
|
labels.push(GRANULARITY_DATE_FORMAT_MASK[props.granularity](entry.date));
|
||||||
|
|
||||||
const value = transformInsightsAverageRunTime(entry.values.averageRunTime);
|
const value = transformInsightsAverageRunTime(entry.values.averageRunTime);
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { generateBarChartOptions } from '@/features/insights/chartjs.utils';
|
import { generateBarChartOptions } from '@/features/insights/chartjs.utils';
|
||||||
import { DATE_FORMAT_MASK } from '@/features/insights/insights.constants';
|
import { GRANULARITY_DATE_FORMAT_MASK } from '@/features/insights/insights.constants';
|
||||||
import type { InsightsByTime, InsightsSummaryType } from '@n8n/api-types';
|
|
||||||
import { smartDecimal } from '@n8n/utils/number/smartDecimal';
|
import { smartDecimal } from '@n8n/utils/number/smartDecimal';
|
||||||
import { useCssVar } from '@vueuse/core';
|
import { useCssVar } from '@vueuse/core';
|
||||||
import type { ChartData } from 'chart.js';
|
import type { ChartData } from 'chart.js';
|
||||||
import dateformat from 'dateformat';
|
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { Bar } from 'vue-chartjs';
|
import { Bar } from 'vue-chartjs';
|
||||||
|
|
||||||
const props = defineProps<{
|
import type { ChartProps } from './insightChartProps';
|
||||||
data: InsightsByTime[];
|
|
||||||
type: InsightsSummaryType;
|
const props = defineProps<ChartProps>();
|
||||||
}>();
|
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
@@ -38,7 +35,7 @@ const chartData = computed<ChartData<'bar'>>(() => {
|
|||||||
const data: number[] = [];
|
const data: number[] = [];
|
||||||
|
|
||||||
for (const entry of props.data) {
|
for (const entry of props.data) {
|
||||||
labels.push(dateformat(entry.date, DATE_FORMAT_MASK));
|
labels.push(GRANULARITY_DATE_FORMAT_MASK[props.granularity](entry.date));
|
||||||
data.push(entry.values.failed);
|
data.push(entry.values.failed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { generateBarChartOptions } from '@/features/insights/chartjs.utils';
|
import { generateBarChartOptions } from '@/features/insights/chartjs.utils';
|
||||||
import { DATE_FORMAT_MASK, INSIGHTS_UNIT_MAPPING } from '@/features/insights/insights.constants';
|
import {
|
||||||
|
GRANULARITY_DATE_FORMAT_MASK,
|
||||||
|
INSIGHTS_UNIT_MAPPING,
|
||||||
|
} from '@/features/insights/insights.constants';
|
||||||
import { transformInsightsFailureRate } from '@/features/insights/insights.utils';
|
import { transformInsightsFailureRate } from '@/features/insights/insights.utils';
|
||||||
import type { InsightsByTime, InsightsSummaryType } from '@n8n/api-types';
|
|
||||||
import { smartDecimal } from '@n8n/utils/number/smartDecimal';
|
import { smartDecimal } from '@n8n/utils/number/smartDecimal';
|
||||||
import { useCssVar } from '@vueuse/core';
|
import { useCssVar } from '@vueuse/core';
|
||||||
import type { ChartData } from 'chart.js';
|
import type { ChartData } from 'chart.js';
|
||||||
import dateformat from 'dateformat';
|
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { Bar } from 'vue-chartjs';
|
import { Bar } from 'vue-chartjs';
|
||||||
|
import type { ChartProps } from './insightChartProps';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<ChartProps>();
|
||||||
data: InsightsByTime[];
|
|
||||||
type: InsightsSummaryType;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
@@ -39,7 +38,7 @@ const chartData = computed<ChartData<'bar'>>(() => {
|
|||||||
const data: number[] = [];
|
const data: number[] = [];
|
||||||
|
|
||||||
for (const entry of props.data) {
|
for (const entry of props.data) {
|
||||||
labels.push(dateformat(entry.date, DATE_FORMAT_MASK));
|
labels.push(GRANULARITY_DATE_FORMAT_MASK[props.granularity](entry.date));
|
||||||
data.push(transformInsightsFailureRate(entry.values.failureRate));
|
data.push(transformInsightsFailureRate(entry.values.failureRate));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,18 +6,17 @@ import {
|
|||||||
} from '@/features/insights/chartjs.utils';
|
} from '@/features/insights/chartjs.utils';
|
||||||
import { transformInsightsTimeSaved } from '@/features/insights/insights.utils';
|
import { transformInsightsTimeSaved } from '@/features/insights/insights.utils';
|
||||||
|
|
||||||
import { DATE_FORMAT_MASK, INSIGHTS_UNIT_MAPPING } from '@/features/insights/insights.constants';
|
import {
|
||||||
import type { InsightsByTime, InsightsSummaryType } from '@n8n/api-types';
|
GRANULARITY_DATE_FORMAT_MASK,
|
||||||
|
INSIGHTS_UNIT_MAPPING,
|
||||||
|
} from '@/features/insights/insights.constants';
|
||||||
import { type ChartData, Filler, type ScriptableContext } from 'chart.js';
|
import { type ChartData, Filler, type ScriptableContext } from 'chart.js';
|
||||||
import dateformat from 'dateformat';
|
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { Line } from 'vue-chartjs';
|
import { Line } from 'vue-chartjs';
|
||||||
|
|
||||||
const props = defineProps<{
|
import type { ChartProps } from './insightChartProps';
|
||||||
data: InsightsByTime[];
|
|
||||||
type: InsightsSummaryType;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
|
const props = defineProps<ChartProps>();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
const chartOptions = computed(() =>
|
const chartOptions = computed(() =>
|
||||||
@@ -51,7 +50,7 @@ const chartData = computed<ChartData<'line'>>(() => {
|
|||||||
const data: number[] = [];
|
const data: number[] = [];
|
||||||
|
|
||||||
for (const entry of props.data) {
|
for (const entry of props.data) {
|
||||||
labels.push(dateformat(entry.date, DATE_FORMAT_MASK));
|
labels.push(GRANULARITY_DATE_FORMAT_MASK[props.granularity](entry.date));
|
||||||
data.push(entry.values.timeSaved);
|
data.push(entry.values.timeSaved);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { generateBarChartOptions } from '@/features/insights/chartjs.utils';
|
import { generateBarChartOptions } from '@/features/insights/chartjs.utils';
|
||||||
import { DATE_FORMAT_MASK } from '@/features/insights/insights.constants';
|
import { GRANULARITY_DATE_FORMAT_MASK } from '@/features/insights/insights.constants';
|
||||||
import type { InsightsByTime, InsightsSummaryType } from '@n8n/api-types';
|
|
||||||
import { useCssVar } from '@vueuse/core';
|
import { useCssVar } from '@vueuse/core';
|
||||||
import type { ChartData } from 'chart.js';
|
import type { ChartData } from 'chart.js';
|
||||||
import dateformat from 'dateformat';
|
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { Bar } from 'vue-chartjs';
|
import { Bar } from 'vue-chartjs';
|
||||||
|
import type { ChartProps } from './insightChartProps';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<ChartProps>();
|
||||||
data: InsightsByTime[];
|
|
||||||
type: InsightsSummaryType;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
@@ -33,7 +29,7 @@ const chartData = computed<ChartData<'bar'>>(() => {
|
|||||||
const failedData: number[] = [];
|
const failedData: number[] = [];
|
||||||
|
|
||||||
for (const entry of props.data) {
|
for (const entry of props.data) {
|
||||||
labels.push(dateformat(entry.date, DATE_FORMAT_MASK));
|
labels.push(GRANULARITY_DATE_FORMAT_MASK[props.granularity](entry.date));
|
||||||
succeededData.push(entry.values.succeeded);
|
succeededData.push(entry.values.succeeded);
|
||||||
failedData.push(entry.values.failed);
|
failedData.push(entry.values.failed);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import type { InsightsByTime, InsightsSummaryType, InsightsDateRange } from '@n8n/api-types';
|
||||||
|
|
||||||
|
export type ChartProps = {
|
||||||
|
data: InsightsByTime[];
|
||||||
|
type: InsightsSummaryType;
|
||||||
|
granularity: InsightsDateRange['granularity'];
|
||||||
|
};
|
||||||
@@ -5,13 +5,20 @@ import type {
|
|||||||
InsightsByTime,
|
InsightsByTime,
|
||||||
InsightsByWorkflow,
|
InsightsByWorkflow,
|
||||||
ListInsightsWorkflowQueryDto,
|
ListInsightsWorkflowQueryDto,
|
||||||
|
InsightsDateRange,
|
||||||
} from '@n8n/api-types';
|
} from '@n8n/api-types';
|
||||||
|
|
||||||
export const fetchInsightsSummary = async (context: IRestApiContext): Promise<InsightsSummary> =>
|
export const fetchInsightsSummary = async (
|
||||||
await makeRestApiRequest(context, 'GET', '/insights/summary');
|
context: IRestApiContext,
|
||||||
|
filter?: { dateRange: InsightsDateRange['key'] },
|
||||||
|
): Promise<InsightsSummary> =>
|
||||||
|
await makeRestApiRequest(context, 'GET', '/insights/summary', filter);
|
||||||
|
|
||||||
export const fetchInsightsByTime = async (context: IRestApiContext): Promise<InsightsByTime[]> =>
|
export const fetchInsightsByTime = async (
|
||||||
await makeRestApiRequest(context, 'GET', '/insights/by-time');
|
context: IRestApiContext,
|
||||||
|
filter?: { dateRange: InsightsDateRange['key'] },
|
||||||
|
): Promise<InsightsByTime[]> =>
|
||||||
|
await makeRestApiRequest(context, 'GET', '/insights/by-time', filter);
|
||||||
|
|
||||||
export const fetchInsightsByWorkflow = async (
|
export const fetchInsightsByWorkflow = async (
|
||||||
context: IRestApiContext,
|
context: IRestApiContext,
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import type { InsightsSummaryType } from '@n8n/api-types';
|
import type { InsightsSummaryType } from '@n8n/api-types';
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
import dateformat from 'dateformat';
|
||||||
|
|
||||||
export const INSIGHTS_SUMMARY_ORDER: InsightsSummaryType[] = [
|
export const INSIGHTS_SUMMARY_ORDER: InsightsSummaryType[] = [
|
||||||
'total',
|
'total',
|
||||||
@@ -44,4 +46,39 @@ export const INSIGHTS_UNIT_IMPACT_MAPPING: Record<
|
|||||||
averageRunTime: INSIGHT_IMPACT_TYPES.NEUTRAL, // Not good or bad → neutral (grey)
|
averageRunTime: INSIGHT_IMPACT_TYPES.NEUTRAL, // Not good or bad → neutral (grey)
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const DATE_FORMAT_MASK = 'mmm d';
|
export const GRANULARITY_DATE_FORMAT_MASK = {
|
||||||
|
hour: (date: string) => dateformat(date, 'HH:MM'),
|
||||||
|
day: (date: string) => dateformat(date, 'mmm d'),
|
||||||
|
week: (date: string) => {
|
||||||
|
const startDate = new Date(date);
|
||||||
|
const endDate = new Date(startDate);
|
||||||
|
endDate.setDate(startDate.getDate() + 7);
|
||||||
|
|
||||||
|
const spansTwoMonths = startDate.getMonth() !== endDate.getMonth();
|
||||||
|
const endDateFormat = spansTwoMonths ? 'mmm d' : 'd';
|
||||||
|
|
||||||
|
return [dateformat(startDate, 'mmm d'), dateformat(endDate, endDateFormat)].join('-');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TELEMETRY_TIME_RANGE = {
|
||||||
|
day: 1,
|
||||||
|
week: 7,
|
||||||
|
'2weeks': 14,
|
||||||
|
month: 30,
|
||||||
|
quarter: 90,
|
||||||
|
'6months': 180,
|
||||||
|
year: 365,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TIME_RANGE_LABELS = {
|
||||||
|
day: useI18n().baseText('insights.lastNHours', { interpolate: { count: 24 } }),
|
||||||
|
week: useI18n().baseText('insights.lastNDays', { interpolate: { count: 7 } }),
|
||||||
|
'2weeks': useI18n().baseText('insights.lastNDays', { interpolate: { count: 14 } }),
|
||||||
|
month: useI18n().baseText('insights.lastNDays', { interpolate: { count: 30 } }),
|
||||||
|
quarter: useI18n().baseText('insights.lastNDays', { interpolate: { count: 90 } }),
|
||||||
|
'6months': useI18n().baseText('insights.months', { interpolate: { count: 6 } }),
|
||||||
|
year: useI18n().baseText('insights.oneYear'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UNLICENSED_TIME_RANGE = 'UNLICENSED_TIME_RANGE' as const;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { useAsyncState } from '@vueuse/core';
|
import { useAsyncState } from '@vueuse/core';
|
||||||
import type { ListInsightsWorkflowQueryDto } from '@n8n/api-types';
|
import type { ListInsightsWorkflowQueryDto, InsightsDateRange } from '@n8n/api-types';
|
||||||
import * as insightsApi from '@/features/insights/insights.api';
|
import * as insightsApi from '@/features/insights/insights.api';
|
||||||
import { useRootStore } from '@/stores/root.store';
|
import { useRootStore } from '@/stores/root.store';
|
||||||
import { useUsersStore } from '@/stores/users.store';
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
@@ -25,9 +25,20 @@ export const useInsightsStore = defineStore('insights', () => {
|
|||||||
() => globalInsightsPermissions.value.list && isInsightsEnabled.value,
|
() => globalInsightsPermissions.value.list && isInsightsEnabled.value,
|
||||||
);
|
);
|
||||||
|
|
||||||
const summary = useAsyncState(
|
const weeklySummary = useAsyncState(
|
||||||
async () => {
|
async () => {
|
||||||
const raw = await insightsApi.fetchInsightsSummary(rootStore.restApiContext);
|
const raw = await insightsApi.fetchInsightsSummary(rootStore.restApiContext, {
|
||||||
|
dateRange: 'week',
|
||||||
|
});
|
||||||
|
return transformInsightsSummary(raw);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
{ immediate: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
const summary = useAsyncState(
|
||||||
|
async (filter?: { dateRange: InsightsDateRange['key'] }) => {
|
||||||
|
const raw = await insightsApi.fetchInsightsSummary(rootStore.restApiContext, filter);
|
||||||
return transformInsightsSummary(raw);
|
return transformInsightsSummary(raw);
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
@@ -35,8 +46,8 @@ export const useInsightsStore = defineStore('insights', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const charts = useAsyncState(
|
const charts = useAsyncState(
|
||||||
async () => {
|
async (filter?: { dateRange: InsightsDateRange['key'] }) => {
|
||||||
return await insightsApi.fetchInsightsByTime(rootStore.restApiContext);
|
return await insightsApi.fetchInsightsByTime(rootStore.restApiContext, filter);
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
{ immediate: false },
|
{ immediate: false },
|
||||||
@@ -53,13 +64,17 @@ export const useInsightsStore = defineStore('insights', () => {
|
|||||||
{ resetOnExecute: false, immediate: false },
|
{ resetOnExecute: false, immediate: false },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const dateRanges = computed(() => settingsStore.settings.insights.dateRanges);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
globalInsightsPermissions,
|
globalInsightsPermissions,
|
||||||
isInsightsEnabled,
|
isInsightsEnabled,
|
||||||
isSummaryEnabled,
|
isSummaryEnabled,
|
||||||
isDashboardEnabled,
|
isDashboardEnabled,
|
||||||
|
weeklySummary,
|
||||||
summary,
|
summary,
|
||||||
charts,
|
charts,
|
||||||
table,
|
table,
|
||||||
|
dateRanges,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ export async function initializeAuthenticatedFeatures(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (insightsStore.isSummaryEnabled) {
|
if (insightsStore.isSummaryEnabled) {
|
||||||
void insightsStore.summary.execute();
|
void insightsStore.weeklySummary.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
|||||||
@@ -3091,6 +3091,9 @@
|
|||||||
"freeAi.credits.showWarning.workflow.activation.title": "You're using free OpenAI API credits",
|
"freeAi.credits.showWarning.workflow.activation.title": "You're using free OpenAI API credits",
|
||||||
"freeAi.credits.showWarning.workflow.activation.description": "To make sure your workflow runs smoothly in the future, replace the free OpenAI API credits with your own API key.",
|
"freeAi.credits.showWarning.workflow.activation.description": "To make sure your workflow runs smoothly in the future, replace the free OpenAI API credits with your own API key.",
|
||||||
"insights.lastNDays": "Last {count} days",
|
"insights.lastNDays": "Last {count} days",
|
||||||
|
"insights.lastNHours": "Last {count} hours",
|
||||||
|
"insights.months": "{count} months",
|
||||||
|
"insights.oneYear": "One year",
|
||||||
"insights.banner.timeSaved.tooltip": "No estimate available yet. To see potential time savings, {link} to each workflow from workflow settings.",
|
"insights.banner.timeSaved.tooltip": "No estimate available yet. To see potential time savings, {link} to each workflow from workflow settings.",
|
||||||
"insights.banner.timeSaved.tooltip.link.text": "add time estimates",
|
"insights.banner.timeSaved.tooltip.link.text": "add time estimates",
|
||||||
"insights.banner.noData": "Collecting...",
|
"insights.banner.noData": "Collecting...",
|
||||||
@@ -3111,5 +3114,12 @@
|
|||||||
"insights.banner.failureRate.deviation.tooltip": "Percentage point change from previous period",
|
"insights.banner.failureRate.deviation.tooltip": "Percentage point change from previous period",
|
||||||
"insights.chart.failed": "Failed",
|
"insights.chart.failed": "Failed",
|
||||||
"insights.chart.succeeded": "Successful",
|
"insights.chart.succeeded": "Successful",
|
||||||
"insights.chart.loading": "Loading data..."
|
"insights.chart.loading": "Loading data...",
|
||||||
|
"insights.upgradeModal.button.dismiss": "Dismiss",
|
||||||
|
"insights.upgradeModal.button.upgrade": "Upgrade",
|
||||||
|
"insights.upgradeModal.content": "Viewing this time period requires an enterprise plan. Upgrade to Enterprise to unlock advanced features.",
|
||||||
|
"insights.upgradeModal.perks.0": "View up to one year of insights history",
|
||||||
|
"insights.upgradeModal.perks.1": "Zoom into last 24 hours with hourly granularity",
|
||||||
|
"insights.upgradeModal.perks.2": "Gain deeper visibility into workflow trends over time",
|
||||||
|
"insights.upgradeModal.title": "Upgrade to Enterprise"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,40 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, watch } from 'vue';
|
|
||||||
import { useRoute, useRouter, type LocationQueryRaw } from 'vue-router';
|
|
||||||
import type { ICredentialTypeMap } from '@/Interface';
|
|
||||||
import type { ICredentialType, ICredentialsDecrypted } from 'n8n-workflow';
|
|
||||||
import ResourcesListLayout, {
|
|
||||||
type Resource,
|
|
||||||
type BaseFilters,
|
|
||||||
} from '@/components/layouts/ResourcesListLayout.vue';
|
|
||||||
import CredentialCard from '@/components/CredentialCard.vue';
|
import CredentialCard from '@/components/CredentialCard.vue';
|
||||||
|
import ResourcesListLayout, {
|
||||||
|
type BaseFilters,
|
||||||
|
type Resource,
|
||||||
|
} from '@/components/layouts/ResourcesListLayout.vue';
|
||||||
|
import ProjectHeader from '@/components/Projects/ProjectHeader.vue';
|
||||||
|
import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
import { useOverview } from '@/composables/useOverview';
|
||||||
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
import {
|
import {
|
||||||
CREDENTIAL_SELECT_MODAL_KEY,
|
|
||||||
CREDENTIAL_EDIT_MODAL_KEY,
|
CREDENTIAL_EDIT_MODAL_KEY,
|
||||||
|
CREDENTIAL_SELECT_MODAL_KEY,
|
||||||
EnterpriseEditionFeature,
|
EnterpriseEditionFeature,
|
||||||
VIEWS,
|
VIEWS,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import { useUIStore, listenForModalChanges } from '@/stores/ui.store';
|
import InsightsSummary from '@/features/insights/components/InsightsSummary.vue';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useInsightsStore } from '@/features/insights/insights.store';
|
||||||
import { useCredentialsStore } from '@/stores/credentials.store';
|
import type { ICredentialTypeMap } from '@/Interface';
|
||||||
import { useExternalSecretsStore } from '@/stores/externalSecrets.ee.store';
|
|
||||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
|
||||||
import { useProjectsStore } from '@/stores/projects.store';
|
|
||||||
import useEnvironmentsStore from '@/stores/environments.ee.store';
|
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
|
||||||
import { useUsersStore } from '@/stores/users.store';
|
|
||||||
import { getResourcePermissions } from '@/permissions';
|
import { getResourcePermissions } from '@/permissions';
|
||||||
import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||||
import { useTelemetry } from '@/composables/useTelemetry';
|
import useEnvironmentsStore from '@/stores/environments.ee.store';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useExternalSecretsStore } from '@/stores/externalSecrets.ee.store';
|
||||||
import ProjectHeader from '@/components/Projects/ProjectHeader.vue';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
|
import { useProjectsStore } from '@/stores/projects.store';
|
||||||
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
|
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||||
|
import { listenForModalChanges, useUIStore } from '@/stores/ui.store';
|
||||||
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
|
import { isCredentialsResource } from '@/utils/typeGuards';
|
||||||
import { N8nCheckbox } from '@n8n/design-system';
|
import { N8nCheckbox } from '@n8n/design-system';
|
||||||
import { pickBy } from 'lodash-es';
|
import { pickBy } from 'lodash-es';
|
||||||
|
import type { ICredentialType, ICredentialsDecrypted } from 'n8n-workflow';
|
||||||
import { CREDENTIAL_EMPTY_VALUE } from 'n8n-workflow';
|
import { CREDENTIAL_EMPTY_VALUE } from 'n8n-workflow';
|
||||||
import { isCredentialsResource } from '@/utils/typeGuards';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
import { useInsightsStore } from '@/features/insights/insights.store';
|
import { useRoute, useRouter, type LocationQueryRaw } from 'vue-router';
|
||||||
import InsightsSummary from '@/features/insights/components/InsightsSummary.vue';
|
|
||||||
import { useOverview } from '@/composables/useOverview';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
credentialId?: string;
|
credentialId?: string;
|
||||||
@@ -243,8 +243,9 @@ onMounted(() => {
|
|||||||
<ProjectHeader>
|
<ProjectHeader>
|
||||||
<InsightsSummary
|
<InsightsSummary
|
||||||
v-if="overview.isOverviewSubPage && insightsStore.isSummaryEnabled"
|
v-if="overview.isOverviewSubPage && insightsStore.isSummaryEnabled"
|
||||||
:loading="insightsStore.summary.isLoading"
|
:loading="insightsStore.weeklySummary.isLoading"
|
||||||
:summary="insightsStore.summary.state"
|
:summary="insightsStore.weeklySummary.state"
|
||||||
|
time-range="week"
|
||||||
/>
|
/>
|
||||||
</ProjectHeader>
|
</ProjectHeader>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import type { ExecutionFilterType } from '@/Interface';
|
||||||
|
import ProjectHeader from '@/components/Projects/ProjectHeader.vue';
|
||||||
|
import GlobalExecutionsList from '@/components/executions/global/GlobalExecutionsList.vue';
|
||||||
|
import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
||||||
|
import { useExternalHooks } from '@/composables/useExternalHooks';
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
import { useOverview } from '@/composables/useOverview';
|
||||||
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
|
import { useToast } from '@/composables/useToast';
|
||||||
|
import InsightsSummary from '@/features/insights/components/InsightsSummary.vue';
|
||||||
|
import { useInsightsStore } from '@/features/insights/insights.store';
|
||||||
|
import { useExecutionsStore } from '@/stores/executions.store';
|
||||||
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
import { onBeforeMount, onBeforeUnmount, onMounted } from 'vue';
|
import { onBeforeMount, onBeforeUnmount, onMounted } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import GlobalExecutionsList from '@/components/executions/global/GlobalExecutionsList.vue';
|
|
||||||
import InsightsSummary from '@/features/insights/components/InsightsSummary.vue';
|
|
||||||
import ProjectHeader from '@/components/Projects/ProjectHeader.vue';
|
|
||||||
import { useI18n } from '@/composables/useI18n';
|
|
||||||
import { useTelemetry } from '@/composables/useTelemetry';
|
|
||||||
import { useExternalHooks } from '@/composables/useExternalHooks';
|
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
|
||||||
import { useExecutionsStore } from '@/stores/executions.store';
|
|
||||||
import { useInsightsStore } from '@/features/insights/insights.store';
|
|
||||||
import { useToast } from '@/composables/useToast';
|
|
||||||
import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
|
||||||
import { storeToRefs } from 'pinia';
|
|
||||||
import type { ExecutionFilterType } from '@/Interface';
|
|
||||||
import { useOverview } from '@/composables/useOverview';
|
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
@@ -97,8 +97,9 @@ async function onExecutionStop() {
|
|||||||
<ProjectHeader>
|
<ProjectHeader>
|
||||||
<InsightsSummary
|
<InsightsSummary
|
||||||
v-if="overview.isOverviewSubPage && insightsStore.isSummaryEnabled"
|
v-if="overview.isOverviewSubPage && insightsStore.isSummaryEnabled"
|
||||||
:loading="insightsStore.summary.isLoading"
|
:loading="insightsStore.weeklySummary.isLoading"
|
||||||
:summary="insightsStore.summary.state"
|
:summary="insightsStore.weeklySummary.state"
|
||||||
|
time-range="week"
|
||||||
/>
|
/>
|
||||||
</ProjectHeader>
|
</ProjectHeader>
|
||||||
</GlobalExecutionsList>
|
</GlobalExecutionsList>
|
||||||
|
|||||||
@@ -1,44 +1,56 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, onMounted, watch, ref, onBeforeUnmount } from 'vue';
|
import Draggable from '@/components/Draggable.vue';
|
||||||
import ResourcesListLayout from '@/components/layouts/ResourcesListLayout.vue';
|
import { FOLDER_LIST_ITEM_ACTIONS } from '@/components/Folders/constants';
|
||||||
import type {
|
import type {
|
||||||
Resource,
|
|
||||||
BaseFilters,
|
BaseFilters,
|
||||||
FolderResource,
|
FolderResource,
|
||||||
|
Resource,
|
||||||
WorkflowResource,
|
WorkflowResource,
|
||||||
} from '@/components/layouts/ResourcesListLayout.vue';
|
} from '@/components/layouts/ResourcesListLayout.vue';
|
||||||
|
import ResourcesListLayout from '@/components/layouts/ResourcesListLayout.vue';
|
||||||
|
import ProjectHeader from '@/components/Projects/ProjectHeader.vue';
|
||||||
import WorkflowCard from '@/components/WorkflowCard.vue';
|
import WorkflowCard from '@/components/WorkflowCard.vue';
|
||||||
import WorkflowTagsDropdown from '@/components/WorkflowTagsDropdown.vue';
|
import WorkflowTagsDropdown from '@/components/WorkflowTagsDropdown.vue';
|
||||||
import Draggable from '@/components/Draggable.vue';
|
import { useDebounce } from '@/composables/useDebounce';
|
||||||
|
import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
||||||
|
import type { DragTarget, DropTarget } from '@/composables/useFolders';
|
||||||
|
import { useFolders } from '@/composables/useFolders';
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
import { useMessage } from '@/composables/useMessage';
|
||||||
|
import { useOverview } from '@/composables/useOverview';
|
||||||
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
|
import { useToast } from '@/composables/useToast';
|
||||||
import {
|
import {
|
||||||
|
COMMUNITY_PLUS_ENROLLMENT_MODAL,
|
||||||
|
DEFAULT_WORKFLOW_PAGE_SIZE,
|
||||||
EASY_AI_WORKFLOW_EXPERIMENT,
|
EASY_AI_WORKFLOW_EXPERIMENT,
|
||||||
EnterpriseEditionFeature,
|
EnterpriseEditionFeature,
|
||||||
VIEWS,
|
|
||||||
DEFAULT_WORKFLOW_PAGE_SIZE,
|
|
||||||
MODAL_CONFIRM,
|
MODAL_CONFIRM,
|
||||||
COMMUNITY_PLUS_ENROLLMENT_MODAL,
|
VIEWS,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
|
import InsightsSummary from '@/features/insights/components/InsightsSummary.vue';
|
||||||
|
import { useInsightsStore } from '@/features/insights/insights.store';
|
||||||
import type {
|
import type {
|
||||||
|
FolderListItem,
|
||||||
|
FolderPathItem,
|
||||||
IUser,
|
IUser,
|
||||||
UserAction,
|
UserAction,
|
||||||
WorkflowListResource,
|
|
||||||
WorkflowListItem,
|
WorkflowListItem,
|
||||||
FolderPathItem,
|
WorkflowListResource,
|
||||||
FolderListItem,
|
|
||||||
} from '@/Interface';
|
} from '@/Interface';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { getResourcePermissions } from '@/permissions';
|
||||||
|
import { useFoldersStore } from '@/stores/folders.store';
|
||||||
|
import { usePostHog } from '@/stores/posthog.store';
|
||||||
|
import { useProjectsStore } from '@/stores/projects.store';
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
import { useUsersStore } from '@/stores/users.store';
|
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
|
||||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||||
import { useTagsStore } from '@/stores/tags.store';
|
import { useTagsStore } from '@/stores/tags.store';
|
||||||
import { useProjectsStore } from '@/stores/projects.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
import { getResourcePermissions } from '@/permissions';
|
import { useUsageStore } from '@/stores/usage.store';
|
||||||
import { usePostHog } from '@/stores/posthog.store';
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { type ProjectSharingData, ProjectTypes } from '@/types/projects.types';
|
||||||
import { type LocationQueryRaw, useRoute, useRouter } from 'vue-router';
|
import { getEasyAiWorkflowJson } from '@/utils/easyAiWorkflowUtils';
|
||||||
import { useTelemetry } from '@/composables/useTelemetry';
|
|
||||||
import {
|
import {
|
||||||
N8nCard,
|
N8nCard,
|
||||||
N8nHeading,
|
N8nHeading,
|
||||||
@@ -48,24 +60,12 @@ import {
|
|||||||
N8nSelect,
|
N8nSelect,
|
||||||
N8nText,
|
N8nText,
|
||||||
} from '@n8n/design-system';
|
} from '@n8n/design-system';
|
||||||
import ProjectHeader from '@/components/Projects/ProjectHeader.vue';
|
|
||||||
import { getEasyAiWorkflowJson } from '@/utils/easyAiWorkflowUtils';
|
|
||||||
import { useDebounce } from '@/composables/useDebounce';
|
|
||||||
import { createEventBus } from '@n8n/utils/event-bus';
|
|
||||||
import type { PathItem } from '@n8n/design-system/components/N8nBreadcrumbs/Breadcrumbs.vue';
|
import type { PathItem } from '@n8n/design-system/components/N8nBreadcrumbs/Breadcrumbs.vue';
|
||||||
import { type ProjectSharingData, ProjectTypes } from '@/types/projects.types';
|
import { createEventBus } from '@n8n/utils/event-bus';
|
||||||
import { FOLDER_LIST_ITEM_ACTIONS } from '@/components/Folders/constants';
|
|
||||||
import { debounce } from 'lodash-es';
|
import { debounce } from 'lodash-es';
|
||||||
import { useMessage } from '@/composables/useMessage';
|
|
||||||
import { useToast } from '@/composables/useToast';
|
|
||||||
import { useFoldersStore } from '@/stores/folders.store';
|
|
||||||
import type { DragTarget, DropTarget } from '@/composables/useFolders';
|
|
||||||
import { useFolders } from '@/composables/useFolders';
|
|
||||||
import { useUsageStore } from '@/stores/usage.store';
|
|
||||||
import { useInsightsStore } from '@/features/insights/insights.store';
|
|
||||||
import InsightsSummary from '@/features/insights/components/InsightsSummary.vue';
|
|
||||||
import { useOverview } from '@/composables/useOverview';
|
|
||||||
import { PROJECT_ROOT } from 'n8n-workflow';
|
import { PROJECT_ROOT } from 'n8n-workflow';
|
||||||
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
|
import { type LocationQueryRaw, useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
const SEARCH_DEBOUNCE_TIME = 300;
|
const SEARCH_DEBOUNCE_TIME = 300;
|
||||||
const FILTERS_DEBOUNCE_TIME = 100;
|
const FILTERS_DEBOUNCE_TIME = 100;
|
||||||
@@ -1337,8 +1337,9 @@ const onCreateWorkflowClick = () => {
|
|||||||
<ProjectHeader @create-folder="createFolderInCurrent">
|
<ProjectHeader @create-folder="createFolderInCurrent">
|
||||||
<InsightsSummary
|
<InsightsSummary
|
||||||
v-if="overview.isOverviewSubPage && insightsStore.isSummaryEnabled"
|
v-if="overview.isOverviewSubPage && insightsStore.isSummaryEnabled"
|
||||||
:loading="insightsStore.summary.isLoading"
|
:loading="insightsStore.weeklySummary.isLoading"
|
||||||
:summary="insightsStore.summary.state"
|
:summary="insightsStore.weeklySummary.state"
|
||||||
|
time-range="week"
|
||||||
/>
|
/>
|
||||||
</ProjectHeader>
|
</ProjectHeader>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user