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 InsightsDateRange } from './schemas/insights.schema';
|
||||
|
||||
export interface IVersionNotificationSettings {
|
||||
enabled: boolean;
|
||||
endpoint: string;
|
||||
@@ -193,5 +195,6 @@ export interface FrontendSettings {
|
||||
enabled: boolean;
|
||||
summary: boolean;
|
||||
dashboard: boolean;
|
||||
dateRanges: InsightsDateRange[];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -251,6 +251,7 @@ export class FrontendService {
|
||||
enabled: this.modulesConfig.modules.includes('insights'),
|
||||
summary: true,
|
||||
dashboard: false,
|
||||
dateRanges: [],
|
||||
},
|
||||
logsView: {
|
||||
enabled: false,
|
||||
|
||||
@@ -149,6 +149,15 @@ export const defaultSettings: FrontendSettings = {
|
||||
enabled: false,
|
||||
summary: true,
|
||||
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: {
|
||||
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">
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import InsightsSummary from '@/features/insights/components/InsightsSummary.vue';
|
||||
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 { TELEMETRY_TIME_RANGE, UNLICENSED_TIME_RANGE } from '../insights.constants';
|
||||
import InsightsDateRangeSelect from './InsightsDateRangeSelect.vue';
|
||||
import InsightsUpgradeModal from './InsightsUpgradeModal.vue';
|
||||
|
||||
const InsightsPaywall = defineAsyncComponent(
|
||||
async () => await import('@/features/insights/components/InsightsPaywall.vue'),
|
||||
@@ -32,6 +36,7 @@ const props = defineProps<{
|
||||
}>();
|
||||
|
||||
const i18n = useI18n();
|
||||
const telemetry = useTelemetry();
|
||||
|
||||
const insightsStore = useInsightsStore();
|
||||
|
||||
@@ -53,10 +58,12 @@ const fetchPaginatedTableData = ({
|
||||
page = 0,
|
||||
itemsPerPage = 20,
|
||||
sortBy,
|
||||
dateRange = selectedDateRange.value,
|
||||
}: {
|
||||
page?: number;
|
||||
itemsPerPage?: number;
|
||||
sortBy: Array<{ id: string; desc: boolean }>;
|
||||
dateRange?: InsightsDateRange['key'];
|
||||
}) => {
|
||||
const skip = page * itemsPerPage;
|
||||
const take = itemsPerPage;
|
||||
@@ -67,22 +74,42 @@ const fetchPaginatedTableData = ({
|
||||
skip,
|
||||
take,
|
||||
sortBy: sortKey,
|
||||
dateRange,
|
||||
});
|
||||
};
|
||||
|
||||
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(
|
||||
() => props.insightType,
|
||||
() => [props.insightType, selectedDateRange.value],
|
||||
() => {
|
||||
sortTableBy.value = [{ id: props.insightType, desc: true }];
|
||||
|
||||
if (insightsStore.isSummaryEnabled) {
|
||||
void insightsStore.summary.execute();
|
||||
void insightsStore.summary.execute(0, { dateRange: selectedDateRange.value });
|
||||
}
|
||||
|
||||
if (insightsStore.isDashboardEnabled) {
|
||||
void insightsStore.charts.execute();
|
||||
fetchPaginatedTableData({ sortBy: sortTableBy.value });
|
||||
void insightsStore.charts.execute(0, { dateRange: selectedDateRange.value });
|
||||
fetchPaginatedTableData({ sortBy: sortTableBy.value, dateRange: selectedDateRange.value });
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -97,51 +124,63 @@ watch(
|
||||
<N8nHeading bold tag="h2" size="xlarge">
|
||||
{{ i18n.baseText('insights.dashboard.title') }}
|
||||
</N8nHeading>
|
||||
<div>
|
||||
<InsightsSummary
|
||||
v-if="insightsStore.isSummaryEnabled"
|
||||
:summary="insightsStore.summary.state"
|
||||
:loading="insightsStore.summary.isLoading"
|
||||
:class="$style.insightsBanner"
|
||||
|
||||
<div class="mt-s">
|
||||
<InsightsDateRangeSelect
|
||||
:model-value="selectedDateRange"
|
||||
style="width: 173px"
|
||||
data-test-id="range-select"
|
||||
@update:model-value="handleTimeChange"
|
||||
/>
|
||||
<div :class="$style.insightsContent">
|
||||
<InsightsPaywall
|
||||
v-if="!insightsStore.isDashboardEnabled"
|
||||
data-test-id="insights-dashboard-unlicensed"
|
||||
/>
|
||||
<div v-else>
|
||||
<div :class="$style.insightsChartWrapper">
|
||||
<div v-if="insightsStore.charts.isLoading" :class="$style.chartLoader">
|
||||
<svg
|
||||
width="22"
|
||||
height="22"
|
||||
viewBox="0 0 22 22"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<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"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
/>
|
||||
</svg>
|
||||
{{ i18n.baseText('insights.chart.loading') }}
|
||||
</div>
|
||||
<component
|
||||
:is="chartComponents[props.insightType]"
|
||||
v-else
|
||||
:type="props.insightType"
|
||||
:data="insightsStore.charts.state"
|
||||
/>
|
||||
</div>
|
||||
<div :class="$style.insightsTableWrapper">
|
||||
<InsightsTableWorkflows
|
||||
v-model:sort-by="sortTableBy"
|
||||
:data="insightsStore.table.state"
|
||||
:loading="insightsStore.table.isLoading"
|
||||
@update:options="fetchPaginatedTableData"
|
||||
/>
|
||||
|
||||
<InsightsUpgradeModal v-model="upgradeModalVisible" />
|
||||
</div>
|
||||
|
||||
<InsightsSummary
|
||||
v-if="insightsStore.isSummaryEnabled"
|
||||
:summary="insightsStore.summary.state"
|
||||
:loading="insightsStore.summary.isLoading"
|
||||
:time-range="selectedDateRange"
|
||||
:class="$style.insightsBanner"
|
||||
/>
|
||||
<div :class="$style.insightsContent">
|
||||
<InsightsPaywall
|
||||
v-if="!insightsStore.isDashboardEnabled"
|
||||
data-test-id="insights-dashboard-unlicensed"
|
||||
/>
|
||||
<div v-else>
|
||||
<div :class="$style.insightsChartWrapper">
|
||||
<div v-if="insightsStore.charts.isLoading" :class="$style.chartLoader">
|
||||
<svg
|
||||
width="22"
|
||||
height="22"
|
||||
viewBox="0 0 22 22"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<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"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
/>
|
||||
</svg>
|
||||
{{ i18n.baseText('insights.chart.loading') }}
|
||||
</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>
|
||||
|
||||
@@ -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({
|
||||
props: {
|
||||
summary: [],
|
||||
timeRange: 'week',
|
||||
},
|
||||
}),
|
||||
).not.toThrow();
|
||||
@@ -95,6 +96,7 @@ describe('InsightsSummary', () => {
|
||||
const { html } = renderComponent({
|
||||
props: {
|
||||
summary,
|
||||
timeRange: 'week',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -5,15 +5,17 @@ import { VIEWS } from '@/constants';
|
||||
import {
|
||||
INSIGHT_IMPACT_TYPES,
|
||||
INSIGHTS_UNIT_IMPACT_MAPPING,
|
||||
TIME_RANGE_LABELS,
|
||||
} from '@/features/insights/insights.constants';
|
||||
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 { computed, ref, useCssModule } from 'vue';
|
||||
import { computed, useCssModule } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const props = defineProps<{
|
||||
summary: InsightsSummaryDisplay;
|
||||
timeRange: InsightsDateRange['key'];
|
||||
loading?: boolean;
|
||||
}>();
|
||||
|
||||
@@ -22,8 +24,6 @@ const route = useRoute();
|
||||
const $style = useCssModule();
|
||||
const telemetry = useTelemetry();
|
||||
|
||||
const lastNDays = ref(7);
|
||||
|
||||
const summaryTitles = computed<Record<keyof InsightsSummary, string>>(() => ({
|
||||
total: i18n.baseText('insights.banner.title.total'),
|
||||
failed: i18n.baseText('insights.banner.title.failed'),
|
||||
@@ -84,9 +84,9 @@ const trackTabClick = (insightType: keyof InsightsSummary) => {
|
||||
{{ summaryTitles[id] }}
|
||||
</N8nTooltip>
|
||||
</strong>
|
||||
<small :class="$style.days">{{
|
||||
i18n.baseText('insights.lastNDays', { interpolate: { count: lastNDays } })
|
||||
}}</small>
|
||||
<small :class="$style.days">
|
||||
{{ TIME_RANGE_LABELS[timeRange] }}
|
||||
</small>
|
||||
<span v-if="summaryHasNoData" :class="$style.noData">
|
||||
<N8nTooltip placement="bottom">
|
||||
<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,
|
||||
generateLineChartOptions,
|
||||
} 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 type { InsightsByTime, InsightsSummaryType } from '@n8n/api-types';
|
||||
import { smartDecimal } from '@n8n/utils/number/smartDecimal';
|
||||
import { type ChartData, Filler, type ScriptableContext } from 'chart.js';
|
||||
import dateformat from 'dateformat';
|
||||
import { computed } from 'vue';
|
||||
import { Line } from 'vue-chartjs';
|
||||
import type { ChartProps } from './insightChartProps';
|
||||
|
||||
const props = defineProps<{
|
||||
data: InsightsByTime[];
|
||||
type: InsightsSummaryType;
|
||||
}>();
|
||||
|
||||
const props = defineProps<ChartProps>();
|
||||
const i18n = useI18n();
|
||||
|
||||
const chartOptions = computed(() =>
|
||||
@@ -40,7 +38,7 @@ const chartData = computed<ChartData<'line'>>(() => {
|
||||
const data: number[] = [];
|
||||
|
||||
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);
|
||||
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { generateBarChartOptions } from '@/features/insights/chartjs.utils';
|
||||
import { DATE_FORMAT_MASK } from '@/features/insights/insights.constants';
|
||||
import type { InsightsByTime, InsightsSummaryType } from '@n8n/api-types';
|
||||
import { GRANULARITY_DATE_FORMAT_MASK } from '@/features/insights/insights.constants';
|
||||
import { smartDecimal } from '@n8n/utils/number/smartDecimal';
|
||||
import { useCssVar } from '@vueuse/core';
|
||||
import type { ChartData } from 'chart.js';
|
||||
import dateformat from 'dateformat';
|
||||
import { computed } from 'vue';
|
||||
import { Bar } from 'vue-chartjs';
|
||||
|
||||
const props = defineProps<{
|
||||
data: InsightsByTime[];
|
||||
type: InsightsSummaryType;
|
||||
}>();
|
||||
import type { ChartProps } from './insightChartProps';
|
||||
|
||||
const props = defineProps<ChartProps>();
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
@@ -38,7 +35,7 @@ const chartData = computed<ChartData<'bar'>>(() => {
|
||||
const data: number[] = [];
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
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 type { InsightsByTime, InsightsSummaryType } from '@n8n/api-types';
|
||||
import { smartDecimal } from '@n8n/utils/number/smartDecimal';
|
||||
import { useCssVar } from '@vueuse/core';
|
||||
import type { ChartData } from 'chart.js';
|
||||
import dateformat from 'dateformat';
|
||||
import { computed } from 'vue';
|
||||
import { Bar } from 'vue-chartjs';
|
||||
import type { ChartProps } from './insightChartProps';
|
||||
|
||||
const props = defineProps<{
|
||||
data: InsightsByTime[];
|
||||
type: InsightsSummaryType;
|
||||
}>();
|
||||
const props = defineProps<ChartProps>();
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
@@ -39,7 +38,7 @@ const chartData = computed<ChartData<'bar'>>(() => {
|
||||
const data: number[] = [];
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
|
||||
@@ -6,18 +6,17 @@ import {
|
||||
} from '@/features/insights/chartjs.utils';
|
||||
import { transformInsightsTimeSaved } from '@/features/insights/insights.utils';
|
||||
|
||||
import { DATE_FORMAT_MASK, INSIGHTS_UNIT_MAPPING } from '@/features/insights/insights.constants';
|
||||
import type { InsightsByTime, InsightsSummaryType } from '@n8n/api-types';
|
||||
import {
|
||||
GRANULARITY_DATE_FORMAT_MASK,
|
||||
INSIGHTS_UNIT_MAPPING,
|
||||
} from '@/features/insights/insights.constants';
|
||||
import { type ChartData, Filler, type ScriptableContext } from 'chart.js';
|
||||
import dateformat from 'dateformat';
|
||||
import { computed } from 'vue';
|
||||
import { Line } from 'vue-chartjs';
|
||||
|
||||
const props = defineProps<{
|
||||
data: InsightsByTime[];
|
||||
type: InsightsSummaryType;
|
||||
}>();
|
||||
import type { ChartProps } from './insightChartProps';
|
||||
|
||||
const props = defineProps<ChartProps>();
|
||||
const i18n = useI18n();
|
||||
|
||||
const chartOptions = computed(() =>
|
||||
@@ -51,7 +50,7 @@ const chartData = computed<ChartData<'line'>>(() => {
|
||||
const data: number[] = [];
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { generateBarChartOptions } from '@/features/insights/chartjs.utils';
|
||||
import { DATE_FORMAT_MASK } from '@/features/insights/insights.constants';
|
||||
import type { InsightsByTime, InsightsSummaryType } from '@n8n/api-types';
|
||||
import { GRANULARITY_DATE_FORMAT_MASK } from '@/features/insights/insights.constants';
|
||||
import { useCssVar } from '@vueuse/core';
|
||||
import type { ChartData } from 'chart.js';
|
||||
import dateformat from 'dateformat';
|
||||
import { computed } from 'vue';
|
||||
import { Bar } from 'vue-chartjs';
|
||||
import type { ChartProps } from './insightChartProps';
|
||||
|
||||
const props = defineProps<{
|
||||
data: InsightsByTime[];
|
||||
type: InsightsSummaryType;
|
||||
}>();
|
||||
const props = defineProps<ChartProps>();
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
@@ -33,7 +29,7 @@ const chartData = computed<ChartData<'bar'>>(() => {
|
||||
const failedData: number[] = [];
|
||||
|
||||
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);
|
||||
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,
|
||||
InsightsByWorkflow,
|
||||
ListInsightsWorkflowQueryDto,
|
||||
InsightsDateRange,
|
||||
} from '@n8n/api-types';
|
||||
|
||||
export const fetchInsightsSummary = async (context: IRestApiContext): Promise<InsightsSummary> =>
|
||||
await makeRestApiRequest(context, 'GET', '/insights/summary');
|
||||
export const fetchInsightsSummary = async (
|
||||
context: IRestApiContext,
|
||||
filter?: { dateRange: InsightsDateRange['key'] },
|
||||
): Promise<InsightsSummary> =>
|
||||
await makeRestApiRequest(context, 'GET', '/insights/summary', filter);
|
||||
|
||||
export const fetchInsightsByTime = async (context: IRestApiContext): Promise<InsightsByTime[]> =>
|
||||
await makeRestApiRequest(context, 'GET', '/insights/by-time');
|
||||
export const fetchInsightsByTime = async (
|
||||
context: IRestApiContext,
|
||||
filter?: { dateRange: InsightsDateRange['key'] },
|
||||
): Promise<InsightsByTime[]> =>
|
||||
await makeRestApiRequest(context, 'GET', '/insights/by-time', filter);
|
||||
|
||||
export const fetchInsightsByWorkflow = async (
|
||||
context: IRestApiContext,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { InsightsSummaryType } from '@n8n/api-types';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import dateformat from 'dateformat';
|
||||
|
||||
export const INSIGHTS_SUMMARY_ORDER: InsightsSummaryType[] = [
|
||||
'total',
|
||||
@@ -44,4 +46,39 @@ export const INSIGHTS_UNIT_IMPACT_MAPPING: Record<
|
||||
averageRunTime: INSIGHT_IMPACT_TYPES.NEUTRAL, // Not good or bad → neutral (grey)
|
||||
} 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 { defineStore } from 'pinia';
|
||||
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 { useRootStore } from '@/stores/root.store';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
@@ -25,9 +25,20 @@ export const useInsightsStore = defineStore('insights', () => {
|
||||
() => globalInsightsPermissions.value.list && isInsightsEnabled.value,
|
||||
);
|
||||
|
||||
const summary = useAsyncState(
|
||||
const weeklySummary = useAsyncState(
|
||||
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);
|
||||
},
|
||||
[],
|
||||
@@ -35,8 +46,8 @@ export const useInsightsStore = defineStore('insights', () => {
|
||||
);
|
||||
|
||||
const charts = useAsyncState(
|
||||
async () => {
|
||||
return await insightsApi.fetchInsightsByTime(rootStore.restApiContext);
|
||||
async (filter?: { dateRange: InsightsDateRange['key'] }) => {
|
||||
return await insightsApi.fetchInsightsByTime(rootStore.restApiContext, filter);
|
||||
},
|
||||
[],
|
||||
{ immediate: false },
|
||||
@@ -53,13 +64,17 @@ export const useInsightsStore = defineStore('insights', () => {
|
||||
{ resetOnExecute: false, immediate: false },
|
||||
);
|
||||
|
||||
const dateRanges = computed(() => settingsStore.settings.insights.dateRanges);
|
||||
|
||||
return {
|
||||
globalInsightsPermissions,
|
||||
isInsightsEnabled,
|
||||
isSummaryEnabled,
|
||||
isDashboardEnabled,
|
||||
weeklySummary,
|
||||
summary,
|
||||
charts,
|
||||
table,
|
||||
dateRanges,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -102,7 +102,7 @@ export async function initializeAuthenticatedFeatures(
|
||||
}
|
||||
|
||||
if (insightsStore.isSummaryEnabled) {
|
||||
void insightsStore.summary.execute();
|
||||
void insightsStore.weeklySummary.execute();
|
||||
}
|
||||
|
||||
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.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.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.link.text": "add time estimates",
|
||||
"insights.banner.noData": "Collecting...",
|
||||
@@ -3111,5 +3114,12 @@
|
||||
"insights.banner.failureRate.deviation.tooltip": "Percentage point change from previous period",
|
||||
"insights.chart.failed": "Failed",
|
||||
"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">
|
||||
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 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 {
|
||||
CREDENTIAL_SELECT_MODAL_KEY,
|
||||
CREDENTIAL_EDIT_MODAL_KEY,
|
||||
CREDENTIAL_SELECT_MODAL_KEY,
|
||||
EnterpriseEditionFeature,
|
||||
VIEWS,
|
||||
} from '@/constants';
|
||||
import { useUIStore, listenForModalChanges } from '@/stores/ui.store';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||
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 InsightsSummary from '@/features/insights/components/InsightsSummary.vue';
|
||||
import { useInsightsStore } from '@/features/insights/insights.store';
|
||||
import type { ICredentialTypeMap } from '@/Interface';
|
||||
import { getResourcePermissions } from '@/permissions';
|
||||
import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import ProjectHeader from '@/components/Projects/ProjectHeader.vue';
|
||||
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||
import useEnvironmentsStore from '@/stores/environments.ee.store';
|
||||
import { useExternalSecretsStore } from '@/stores/externalSecrets.ee.store';
|
||||
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 { pickBy } from 'lodash-es';
|
||||
import type { ICredentialType, ICredentialsDecrypted } from 'n8n-workflow';
|
||||
import { CREDENTIAL_EMPTY_VALUE } from 'n8n-workflow';
|
||||
import { isCredentialsResource } from '@/utils/typeGuards';
|
||||
import { useInsightsStore } from '@/features/insights/insights.store';
|
||||
import InsightsSummary from '@/features/insights/components/InsightsSummary.vue';
|
||||
import { useOverview } from '@/composables/useOverview';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { useRoute, useRouter, type LocationQueryRaw } from 'vue-router';
|
||||
|
||||
const props = defineProps<{
|
||||
credentialId?: string;
|
||||
@@ -243,8 +243,9 @@ onMounted(() => {
|
||||
<ProjectHeader>
|
||||
<InsightsSummary
|
||||
v-if="overview.isOverviewSubPage && insightsStore.isSummaryEnabled"
|
||||
:loading="insightsStore.summary.isLoading"
|
||||
:summary="insightsStore.summary.state"
|
||||
:loading="insightsStore.weeklySummary.isLoading"
|
||||
:summary="insightsStore.weeklySummary.state"
|
||||
time-range="week"
|
||||
/>
|
||||
</ProjectHeader>
|
||||
</template>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<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 { 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 i18n = useI18n();
|
||||
@@ -97,8 +97,9 @@ async function onExecutionStop() {
|
||||
<ProjectHeader>
|
||||
<InsightsSummary
|
||||
v-if="overview.isOverviewSubPage && insightsStore.isSummaryEnabled"
|
||||
:loading="insightsStore.summary.isLoading"
|
||||
:summary="insightsStore.summary.state"
|
||||
:loading="insightsStore.weeklySummary.isLoading"
|
||||
:summary="insightsStore.weeklySummary.state"
|
||||
time-range="week"
|
||||
/>
|
||||
</ProjectHeader>
|
||||
</GlobalExecutionsList>
|
||||
|
||||
@@ -1,44 +1,56 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, watch, ref, onBeforeUnmount } from 'vue';
|
||||
import ResourcesListLayout from '@/components/layouts/ResourcesListLayout.vue';
|
||||
import Draggable from '@/components/Draggable.vue';
|
||||
import { FOLDER_LIST_ITEM_ACTIONS } from '@/components/Folders/constants';
|
||||
import type {
|
||||
Resource,
|
||||
BaseFilters,
|
||||
FolderResource,
|
||||
Resource,
|
||||
WorkflowResource,
|
||||
} 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 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 {
|
||||
COMMUNITY_PLUS_ENROLLMENT_MODAL,
|
||||
DEFAULT_WORKFLOW_PAGE_SIZE,
|
||||
EASY_AI_WORKFLOW_EXPERIMENT,
|
||||
EnterpriseEditionFeature,
|
||||
VIEWS,
|
||||
DEFAULT_WORKFLOW_PAGE_SIZE,
|
||||
MODAL_CONFIRM,
|
||||
COMMUNITY_PLUS_ENROLLMENT_MODAL,
|
||||
VIEWS,
|
||||
} from '@/constants';
|
||||
import InsightsSummary from '@/features/insights/components/InsightsSummary.vue';
|
||||
import { useInsightsStore } from '@/features/insights/insights.store';
|
||||
import type {
|
||||
FolderListItem,
|
||||
FolderPathItem,
|
||||
IUser,
|
||||
UserAction,
|
||||
WorkflowListResource,
|
||||
WorkflowListItem,
|
||||
FolderPathItem,
|
||||
FolderListItem,
|
||||
WorkflowListResource,
|
||||
} 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 { useUsersStore } from '@/stores/users.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||
import { useTagsStore } from '@/stores/tags.store';
|
||||
import { useProjectsStore } from '@/stores/projects.store';
|
||||
import { getResourcePermissions } from '@/permissions';
|
||||
import { usePostHog } from '@/stores/posthog.store';
|
||||
import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { type LocationQueryRaw, useRoute, useRouter } from 'vue-router';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useUsageStore } from '@/stores/usage.store';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { type ProjectSharingData, ProjectTypes } from '@/types/projects.types';
|
||||
import { getEasyAiWorkflowJson } from '@/utils/easyAiWorkflowUtils';
|
||||
import {
|
||||
N8nCard,
|
||||
N8nHeading,
|
||||
@@ -48,24 +60,12 @@ import {
|
||||
N8nSelect,
|
||||
N8nText,
|
||||
} 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 ProjectSharingData, ProjectTypes } from '@/types/projects.types';
|
||||
import { FOLDER_LIST_ITEM_ACTIONS } from '@/components/Folders/constants';
|
||||
import { createEventBus } from '@n8n/utils/event-bus';
|
||||
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 { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import { type LocationQueryRaw, useRoute, useRouter } from 'vue-router';
|
||||
|
||||
const SEARCH_DEBOUNCE_TIME = 300;
|
||||
const FILTERS_DEBOUNCE_TIME = 100;
|
||||
@@ -1337,8 +1337,9 @@ const onCreateWorkflowClick = () => {
|
||||
<ProjectHeader @create-folder="createFolderInCurrent">
|
||||
<InsightsSummary
|
||||
v-if="overview.isOverviewSubPage && insightsStore.isSummaryEnabled"
|
||||
:loading="insightsStore.summary.isLoading"
|
||||
:summary="insightsStore.summary.state"
|
||||
:loading="insightsStore.weeklySummary.isLoading"
|
||||
:summary="insightsStore.weeklySummary.state"
|
||||
time-range="week"
|
||||
/>
|
||||
</ProjectHeader>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user