@n8n/frontend-module-insights 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +88 -0
- package/README.md +64 -0
- package/biome.jsonc +4 -0
- package/eslint.config.mjs +33 -0
- package/package.json +72 -0
- package/src/__tests__/setup.ts +4 -0
- package/src/chartjs.utils.ts +208 -0
- package/src/components/InsightsDashboard.test.ts +891 -0
- package/src/components/InsightsDashboard.vue +426 -0
- package/src/components/InsightsDataRangePicker.vue +195 -0
- package/src/components/InsightsDateRangeAlert.test.ts +134 -0
- package/src/components/InsightsDateRangeAlert.vue +89 -0
- package/src/components/InsightsPaywall.vue +42 -0
- package/src/components/InsightsSummary.test.ts +206 -0
- package/src/components/InsightsSummary.vue +357 -0
- package/src/components/InsightsUpgradeModal.vue +74 -0
- package/src/components/__snapshots__/InsightsSummary.test.ts.snap +446 -0
- package/src/components/charts/InsightsChartAverageRuntime.vue +67 -0
- package/src/components/charts/InsightsChartFailed.vue +57 -0
- package/src/components/charts/InsightsChartFailureRate.vue +58 -0
- package/src/components/charts/InsightsChartTimeSaved.vue +74 -0
- package/src/components/charts/InsightsChartTotal.vue +58 -0
- package/src/components/charts/insightChartProps.ts +9 -0
- package/src/components/tables/InsightsTableWorkflows.test.ts +509 -0
- package/src/components/tables/InsightsTableWorkflows.vue +279 -0
- package/src/index.ts +4 -0
- package/src/insights.api.test.ts +435 -0
- package/src/insights.api.ts +71 -0
- package/src/insights.constants.ts +69 -0
- package/src/insights.module.test.ts +59 -0
- package/src/insights.module.ts +45 -0
- package/src/insights.store.test.ts +118 -0
- package/src/insights.store.ts +88 -0
- package/src/insights.types.ts +16 -0
- package/src/insights.utils.test.ts +339 -0
- package/src/insights.utils.ts +172 -0
- package/stylelint.config.mjs +9 -0
- package/vite.config.ts +29 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { getLocalTimeZone } from '@internationalized/date';
|
|
2
|
+
import type {
|
|
3
|
+
InsightsSummary,
|
|
4
|
+
InsightsByTime,
|
|
5
|
+
InsightsByWorkflow,
|
|
6
|
+
ListInsightsWorkflowQueryDto,
|
|
7
|
+
InsightsDateFilterDto,
|
|
8
|
+
} from '@n8n/api-types';
|
|
9
|
+
import { makeRestApiRequest } from '@n8n/rest-api-client';
|
|
10
|
+
import type { IRestApiContext } from '@n8n/rest-api-client';
|
|
11
|
+
|
|
12
|
+
type SerializedDateFilter<T> = Omit<T, 'startDate' | 'endDate'> & {
|
|
13
|
+
startDate?: string;
|
|
14
|
+
endDate?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function serializeInsightsFilter<
|
|
18
|
+
T extends InsightsDateFilterDto | ListInsightsWorkflowQueryDto,
|
|
19
|
+
>(filter?: T): SerializedDateFilter<T> | undefined {
|
|
20
|
+
if (!filter) return undefined;
|
|
21
|
+
|
|
22
|
+
const { startDate, endDate, ...rest } = filter;
|
|
23
|
+
const serialized: SerializedDateFilter<T> = { ...rest };
|
|
24
|
+
|
|
25
|
+
if (startDate) {
|
|
26
|
+
serialized.startDate = startDate.toISOString();
|
|
27
|
+
}
|
|
28
|
+
if (endDate) {
|
|
29
|
+
serialized.endDate = endDate.toISOString();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Send the browser timezone so the backend floors custom ranges to the day the
|
|
33
|
+
// user actually selected instead of the UTC calendar day.
|
|
34
|
+
serialized.timeZone = getLocalTimeZone();
|
|
35
|
+
|
|
36
|
+
return serialized;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const fetchInsightsSummary = async (
|
|
40
|
+
context: IRestApiContext,
|
|
41
|
+
filter?: InsightsDateFilterDto,
|
|
42
|
+
): Promise<InsightsSummary> =>
|
|
43
|
+
await makeRestApiRequest(context, 'GET', '/insights/summary', serializeInsightsFilter(filter));
|
|
44
|
+
|
|
45
|
+
export const fetchInsightsByTime = async (
|
|
46
|
+
context: IRestApiContext,
|
|
47
|
+
filter?: InsightsDateFilterDto,
|
|
48
|
+
): Promise<InsightsByTime[]> =>
|
|
49
|
+
await makeRestApiRequest(context, 'GET', '/insights/by-time', serializeInsightsFilter(filter));
|
|
50
|
+
|
|
51
|
+
export const fetchInsightsTimeSaved = async (
|
|
52
|
+
context: IRestApiContext,
|
|
53
|
+
filter?: InsightsDateFilterDto,
|
|
54
|
+
): Promise<InsightsByTime[]> =>
|
|
55
|
+
await makeRestApiRequest(
|
|
56
|
+
context,
|
|
57
|
+
'GET',
|
|
58
|
+
'/insights/by-time/time-saved',
|
|
59
|
+
serializeInsightsFilter(filter),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
export const fetchInsightsByWorkflow = async (
|
|
63
|
+
context: IRestApiContext,
|
|
64
|
+
filter?: ListInsightsWorkflowQueryDto,
|
|
65
|
+
): Promise<InsightsByWorkflow> =>
|
|
66
|
+
await makeRestApiRequest(
|
|
67
|
+
context,
|
|
68
|
+
'GET',
|
|
69
|
+
'/insights/by-workflow',
|
|
70
|
+
serializeInsightsFilter(filter),
|
|
71
|
+
);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { InsightsSummaryType } from '@n8n/api-types';
|
|
2
|
+
import dateformat from 'dateformat';
|
|
3
|
+
|
|
4
|
+
export const INSIGHT_TYPES = {
|
|
5
|
+
TOTAL: 'total',
|
|
6
|
+
FAILED: 'failed',
|
|
7
|
+
FAILURE_RATE: 'failureRate',
|
|
8
|
+
TIME_SAVED: 'timeSaved',
|
|
9
|
+
AVERAGE_RUN_TIME: 'averageRunTime',
|
|
10
|
+
} as const;
|
|
11
|
+
|
|
12
|
+
export const INSIGHTS_SUMMARY_ORDER: InsightsSummaryType[] = [
|
|
13
|
+
INSIGHT_TYPES.TOTAL,
|
|
14
|
+
INSIGHT_TYPES.FAILED,
|
|
15
|
+
INSIGHT_TYPES.FAILURE_RATE,
|
|
16
|
+
INSIGHT_TYPES.TIME_SAVED,
|
|
17
|
+
INSIGHT_TYPES.AVERAGE_RUN_TIME,
|
|
18
|
+
] as const;
|
|
19
|
+
|
|
20
|
+
export const INSIGHTS_UNIT_MAPPING: Record<InsightsSummaryType, (value: number) => string> = {
|
|
21
|
+
total: () => '',
|
|
22
|
+
failed: () => '',
|
|
23
|
+
failureRate: () => '%',
|
|
24
|
+
timeSaved: (value: number) => (Math.abs(value) < 60 ? 'm' : 'h'),
|
|
25
|
+
averageRunTime: () => 's',
|
|
26
|
+
} as const;
|
|
27
|
+
|
|
28
|
+
export const INSIGHTS_DEVIATION_UNIT_MAPPING: Record<
|
|
29
|
+
InsightsSummaryType,
|
|
30
|
+
(deviation: number) => string
|
|
31
|
+
> = {
|
|
32
|
+
total: () => '%',
|
|
33
|
+
failed: () => '%',
|
|
34
|
+
failureRate: () => 'pp',
|
|
35
|
+
timeSaved: (deviation: number) => (Math.abs(deviation) < 60 ? 'm' : 'h'),
|
|
36
|
+
averageRunTime: () => 's',
|
|
37
|
+
} as const;
|
|
38
|
+
|
|
39
|
+
export const INSIGHT_IMPACT_TYPES = {
|
|
40
|
+
POSITIVE: 'positive',
|
|
41
|
+
NEGATIVE: 'negative',
|
|
42
|
+
NEUTRAL: 'neutral',
|
|
43
|
+
} as const;
|
|
44
|
+
|
|
45
|
+
export const INSIGHTS_UNIT_IMPACT_MAPPING: Record<
|
|
46
|
+
InsightsSummaryType,
|
|
47
|
+
(typeof INSIGHT_IMPACT_TYPES)[keyof typeof INSIGHT_IMPACT_TYPES]
|
|
48
|
+
> = {
|
|
49
|
+
total: INSIGHT_IMPACT_TYPES.POSITIVE,
|
|
50
|
+
failed: INSIGHT_IMPACT_TYPES.NEGATIVE,
|
|
51
|
+
failureRate: INSIGHT_IMPACT_TYPES.NEGATIVE, // Higher failureRate is bad → negative (red)
|
|
52
|
+
timeSaved: INSIGHT_IMPACT_TYPES.POSITIVE, // More time saved is good → positive (green)
|
|
53
|
+
averageRunTime: INSIGHT_IMPACT_TYPES.NEUTRAL, // Not good or bad → neutral (grey)
|
|
54
|
+
} as const;
|
|
55
|
+
|
|
56
|
+
export const GRANULARITY_DATE_FORMAT_MASK = {
|
|
57
|
+
hour: (date: string) => dateformat(date, 'HH:MM'),
|
|
58
|
+
day: (date: string) => dateformat(date, 'mmm d'),
|
|
59
|
+
week: (date: string) => {
|
|
60
|
+
const startDate = new Date(date);
|
|
61
|
+
const endDate = new Date(startDate);
|
|
62
|
+
endDate.setDate(startDate.getDate() + 7);
|
|
63
|
+
|
|
64
|
+
const spansTwoMonths = startDate.getMonth() !== endDate.getMonth();
|
|
65
|
+
const endDateFormat = spansTwoMonths ? 'mmm d' : 'd';
|
|
66
|
+
|
|
67
|
+
return [dateformat(startDate, 'mmm d'), dateformat(endDate, endDateFormat)].join('-');
|
|
68
|
+
},
|
|
69
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { VIEWS } from '@n8n/frontend-constants/views';
|
|
2
|
+
import { createMemoryHistory, createRouter, type RouteRecordRaw } from 'vue-router';
|
|
3
|
+
|
|
4
|
+
import { InsightsModule } from './insights.module';
|
|
5
|
+
|
|
6
|
+
const mocks = vi.hoisted(() => ({ isInsightsEnabled: true }));
|
|
7
|
+
|
|
8
|
+
vi.mock('./insights.store', () => ({
|
|
9
|
+
useInsightsStore: () => ({
|
|
10
|
+
get isInsightsEnabled() {
|
|
11
|
+
return mocks.isInsightsEnabled;
|
|
12
|
+
},
|
|
13
|
+
}),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
const stub = { render: () => null };
|
|
17
|
+
|
|
18
|
+
// Swap the lazy dashboard for a stub so navigation doesn't pull the chart tree.
|
|
19
|
+
function withStubbedComponents(route: RouteRecordRaw): RouteRecordRaw {
|
|
20
|
+
const clone = { ...route } as Record<string, unknown>;
|
|
21
|
+
if (Array.isArray(clone.children)) {
|
|
22
|
+
clone.children = (clone.children as RouteRecordRaw[]).map(withStubbedComponents);
|
|
23
|
+
} else if (clone.component) {
|
|
24
|
+
clone.component = stub;
|
|
25
|
+
}
|
|
26
|
+
return clone as unknown as RouteRecordRaw;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function createTestRouter() {
|
|
30
|
+
return createRouter({
|
|
31
|
+
history: createMemoryHistory(),
|
|
32
|
+
routes: [
|
|
33
|
+
...(InsightsModule.routes ?? []).map(withStubbedComponents),
|
|
34
|
+
{ path: '/:pathMatch(.*)*', name: VIEWS.NOT_FOUND, component: stub },
|
|
35
|
+
],
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('InsightsModule routes', () => {
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
mocks.isInsightsEnabled = true;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('enters the dashboard with a default insight type when insights is enabled', async () => {
|
|
45
|
+
const router = createTestRouter();
|
|
46
|
+
await router.push('/insights');
|
|
47
|
+
|
|
48
|
+
expect(router.currentRoute.value.name).toBe(VIEWS.INSIGHTS);
|
|
49
|
+
expect(router.currentRoute.value.params).toEqual({ insightType: 'total' });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('redirects to not found when insights is disabled', async () => {
|
|
53
|
+
mocks.isInsightsEnabled = false;
|
|
54
|
+
const router = createTestRouter();
|
|
55
|
+
await router.push('/insights');
|
|
56
|
+
|
|
57
|
+
expect(router.currentRoute.value.name).toBe(VIEWS.NOT_FOUND);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { VIEWS } from '@n8n/frontend-constants/views';
|
|
2
|
+
import type { FrontendModuleDescription } from '@n8n/frontend-module-sdk';
|
|
3
|
+
import { RouterView } from 'vue-router';
|
|
4
|
+
|
|
5
|
+
const InsightsDashboard = async () => await import('./components/InsightsDashboard.vue');
|
|
6
|
+
|
|
7
|
+
export const InsightsModule: FrontendModuleDescription = {
|
|
8
|
+
id: 'insights',
|
|
9
|
+
name: 'Insights',
|
|
10
|
+
description: 'Provides insights and analytics features for projects.',
|
|
11
|
+
icon: 'chart-column-decreasing',
|
|
12
|
+
routes: [
|
|
13
|
+
{
|
|
14
|
+
path: '/insights',
|
|
15
|
+
// Loaded inside the guard, not at module top level: the descriptor is imported
|
|
16
|
+
// eagerly by the shell manifest, so a static store import would pull the whole
|
|
17
|
+
// feature into the boot chunk even when the module is disabled.
|
|
18
|
+
async beforeEnter() {
|
|
19
|
+
const { useInsightsStore } = await import('./insights.store');
|
|
20
|
+
return useInsightsStore().isInsightsEnabled || { name: VIEWS.NOT_FOUND };
|
|
21
|
+
},
|
|
22
|
+
component: RouterView,
|
|
23
|
+
meta: {
|
|
24
|
+
middleware: ['authenticated', 'rbac'],
|
|
25
|
+
middlewareOptions: {
|
|
26
|
+
rbac: {
|
|
27
|
+
scope: ['insights:list'],
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
children: [
|
|
32
|
+
{
|
|
33
|
+
path: ':insightType?',
|
|
34
|
+
name: VIEWS.INSIGHTS,
|
|
35
|
+
beforeEnter(to) {
|
|
36
|
+
if (to.params.insightType) return true;
|
|
37
|
+
return Object.assign(to, { params: { ...to.params, insightType: 'total' } });
|
|
38
|
+
},
|
|
39
|
+
component: InsightsDashboard,
|
|
40
|
+
props: true,
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { FrontendModuleSettings } from '@n8n/api-types';
|
|
2
|
+
import { mockedStore, type MockedStore } from '@n8n/frontend-test-utils';
|
|
3
|
+
import type { IUser } from '@n8n/rest-api-client/api/users';
|
|
4
|
+
import { useSettingsStore } from '@n8n/stores/settings.store';
|
|
5
|
+
import { useRootStore } from '@n8n/stores/useRootStore';
|
|
6
|
+
import { useUsersStore } from '@n8n/stores/users.store';
|
|
7
|
+
import { createTestingPinia } from '@pinia/testing';
|
|
8
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
9
|
+
import { reactive } from 'vue';
|
|
10
|
+
|
|
11
|
+
import * as insightsApi from './insights.api';
|
|
12
|
+
import { useInsightsStore } from './insights.store';
|
|
13
|
+
|
|
14
|
+
vi.mock('vue-router', async (importOriginal) => ({
|
|
15
|
+
...(await importOriginal()),
|
|
16
|
+
useRoute: () => reactive({}),
|
|
17
|
+
}));
|
|
18
|
+
vi.mock('./insights.api');
|
|
19
|
+
|
|
20
|
+
const mockFilter = { projectId: 'test-project' };
|
|
21
|
+
const mockData = [
|
|
22
|
+
{
|
|
23
|
+
date: '2023-01-01',
|
|
24
|
+
values: {
|
|
25
|
+
total: 100,
|
|
26
|
+
failed: 10,
|
|
27
|
+
failureRate: 10,
|
|
28
|
+
timeSaved: 50,
|
|
29
|
+
averageRunTime: 5,
|
|
30
|
+
succeeded: 90,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
describe('useInsightsStore', () => {
|
|
36
|
+
let insightsStore: ReturnType<typeof useInsightsStore>;
|
|
37
|
+
let settingsStore: MockedStore<typeof useSettingsStore>;
|
|
38
|
+
let rootStore: MockedStore<typeof useRootStore>;
|
|
39
|
+
let usersStore: MockedStore<typeof useUsersStore>;
|
|
40
|
+
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
createTestingPinia();
|
|
43
|
+
|
|
44
|
+
settingsStore = mockedStore(useSettingsStore);
|
|
45
|
+
rootStore = mockedStore(useRootStore);
|
|
46
|
+
usersStore = mockedStore(useUsersStore);
|
|
47
|
+
|
|
48
|
+
rootStore.restApiContext = {
|
|
49
|
+
baseUrl: 'http://localhost',
|
|
50
|
+
pushRef: 'pushRef',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
usersStore.currentUser = { globalScopes: ['insights:list'] } as IUser;
|
|
54
|
+
|
|
55
|
+
insightsStore = useInsightsStore();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
afterEach(() => {
|
|
59
|
+
vi.clearAllMocks();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('charts data fetcher', () => {
|
|
63
|
+
it('should use fetchInsightsTimeSaved when dashboard is disabled', async () => {
|
|
64
|
+
settingsStore.moduleSettings = { insights: { dashboard: false } } as FrontendModuleSettings;
|
|
65
|
+
|
|
66
|
+
vi.mocked(insightsApi.fetchInsightsTimeSaved).mockResolvedValue(mockData);
|
|
67
|
+
|
|
68
|
+
await insightsStore.charts.execute(0, mockFilter);
|
|
69
|
+
|
|
70
|
+
expect(insightsApi.fetchInsightsTimeSaved).toHaveBeenCalledWith(
|
|
71
|
+
rootStore.restApiContext,
|
|
72
|
+
mockFilter,
|
|
73
|
+
);
|
|
74
|
+
expect(insightsApi.fetchInsightsByTime).not.toHaveBeenCalled();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should use fetchInsightsByTime when dashboard is enabled', async () => {
|
|
78
|
+
settingsStore.moduleSettings = { insights: { dashboard: true } } as FrontendModuleSettings;
|
|
79
|
+
|
|
80
|
+
vi.mocked(insightsApi.fetchInsightsByTime).mockResolvedValue(mockData);
|
|
81
|
+
|
|
82
|
+
await insightsStore.charts.execute(0, mockFilter);
|
|
83
|
+
|
|
84
|
+
expect(insightsApi.fetchInsightsByTime).toHaveBeenCalledWith(
|
|
85
|
+
rootStore.restApiContext,
|
|
86
|
+
mockFilter,
|
|
87
|
+
);
|
|
88
|
+
expect(insightsApi.fetchInsightsTimeSaved).not.toHaveBeenCalled();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should use fetchInsightsTimeSaved when dashboard setting is undefined', async () => {
|
|
92
|
+
settingsStore.moduleSettings = { insights: {} } as FrontendModuleSettings;
|
|
93
|
+
|
|
94
|
+
vi.mocked(insightsApi.fetchInsightsTimeSaved).mockResolvedValue(mockData);
|
|
95
|
+
|
|
96
|
+
await insightsStore.charts.execute(0, mockFilter);
|
|
97
|
+
|
|
98
|
+
expect(insightsApi.fetchInsightsTimeSaved).toHaveBeenCalledWith(
|
|
99
|
+
rootStore.restApiContext,
|
|
100
|
+
mockFilter,
|
|
101
|
+
);
|
|
102
|
+
expect(insightsApi.fetchInsightsByTime).not.toHaveBeenCalled();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should work without filter parameter', async () => {
|
|
106
|
+
settingsStore.moduleSettings = { insights: { dashboard: true } } as FrontendModuleSettings;
|
|
107
|
+
|
|
108
|
+
vi.mocked(insightsApi.fetchInsightsByTime).mockResolvedValue(mockData);
|
|
109
|
+
|
|
110
|
+
await insightsStore.charts.execute();
|
|
111
|
+
|
|
112
|
+
expect(insightsApi.fetchInsightsByTime).toHaveBeenCalledWith(
|
|
113
|
+
rootStore.restApiContext,
|
|
114
|
+
undefined,
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { ListInsightsWorkflowQueryDto, InsightsDateFilterDto } from '@n8n/api-types';
|
|
2
|
+
import { getResourcePermissions } from '@n8n/permissions';
|
|
3
|
+
import { useSettingsStore } from '@n8n/stores/settings.store';
|
|
4
|
+
import { useRootStore } from '@n8n/stores/useRootStore';
|
|
5
|
+
import { useUsersStore } from '@n8n/stores/users.store';
|
|
6
|
+
import { useAsyncState } from '@vueuse/core';
|
|
7
|
+
import { defineStore } from 'pinia';
|
|
8
|
+
import { computed } from 'vue';
|
|
9
|
+
|
|
10
|
+
import * as insightsApi from './insights.api';
|
|
11
|
+
import { transformInsightsSummary } from './insights.utils';
|
|
12
|
+
|
|
13
|
+
export const useInsightsStore = defineStore('insights', () => {
|
|
14
|
+
const rootStore = useRootStore();
|
|
15
|
+
const usersStore = useUsersStore();
|
|
16
|
+
const settingsStore = useSettingsStore();
|
|
17
|
+
|
|
18
|
+
const globalInsightsPermissions = computed(
|
|
19
|
+
() => getResourcePermissions(usersStore.currentUser?.globalScopes).insights,
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const isInsightsEnabled = computed(() => settingsStore.isModuleActive('insights'));
|
|
23
|
+
|
|
24
|
+
const isDashboardEnabled = computed(() => !!settingsStore.moduleSettings.insights?.dashboard);
|
|
25
|
+
|
|
26
|
+
const isSummaryEnabled = computed(
|
|
27
|
+
() => globalInsightsPermissions.value.list && isInsightsEnabled.value,
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const weeklySummary = useAsyncState(
|
|
31
|
+
async () => {
|
|
32
|
+
const raw = await insightsApi.fetchInsightsSummary(rootStore.restApiContext);
|
|
33
|
+
return transformInsightsSummary(raw);
|
|
34
|
+
},
|
|
35
|
+
[],
|
|
36
|
+
{ immediate: false, resetOnExecute: false },
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const summary = useAsyncState(
|
|
40
|
+
async (filter?: InsightsDateFilterDto) => {
|
|
41
|
+
const raw = await insightsApi.fetchInsightsSummary(rootStore.restApiContext, filter);
|
|
42
|
+
return transformInsightsSummary(raw);
|
|
43
|
+
},
|
|
44
|
+
[],
|
|
45
|
+
{ immediate: false, resetOnExecute: false },
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const charts = useAsyncState(
|
|
49
|
+
async (filter?: InsightsDateFilterDto) => {
|
|
50
|
+
const dataFetcher = isDashboardEnabled.value
|
|
51
|
+
? insightsApi.fetchInsightsByTime
|
|
52
|
+
: insightsApi.fetchInsightsTimeSaved;
|
|
53
|
+
return await dataFetcher(rootStore.restApiContext, filter);
|
|
54
|
+
},
|
|
55
|
+
[],
|
|
56
|
+
{ immediate: false, resetOnExecute: false },
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const table = useAsyncState(
|
|
60
|
+
async (filter?: ListInsightsWorkflowQueryDto) => {
|
|
61
|
+
return await insightsApi.fetchInsightsByWorkflow(rootStore.restApiContext, filter);
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
count: 0,
|
|
65
|
+
data: [],
|
|
66
|
+
},
|
|
67
|
+
{ immediate: false, resetOnExecute: false },
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const dateRanges = computed(() => settingsStore.moduleSettings.insights?.dateRanges ?? []);
|
|
71
|
+
|
|
72
|
+
const earliestDataDate = computed(
|
|
73
|
+
() => settingsStore.moduleSettings.insights?.earliestDataDate ?? null,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
globalInsightsPermissions,
|
|
78
|
+
isInsightsEnabled,
|
|
79
|
+
isSummaryEnabled,
|
|
80
|
+
isDashboardEnabled,
|
|
81
|
+
weeklySummary,
|
|
82
|
+
summary,
|
|
83
|
+
charts,
|
|
84
|
+
table,
|
|
85
|
+
dateRanges,
|
|
86
|
+
earliestDataDate,
|
|
87
|
+
};
|
|
88
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { INSIGHTS_UNIT_MAPPING, INSIGHTS_DEVIATION_UNIT_MAPPING } from './insights.constants';
|
|
2
|
+
|
|
3
|
+
type InsightsDisplayUnits = typeof INSIGHTS_UNIT_MAPPING;
|
|
4
|
+
type InsightsDisplayDeviationUnits = typeof INSIGHTS_DEVIATION_UNIT_MAPPING;
|
|
5
|
+
|
|
6
|
+
export type InsightsSummaryDisplay = Array<
|
|
7
|
+
{
|
|
8
|
+
[K in keyof InsightsDisplayUnits]: {
|
|
9
|
+
id: K;
|
|
10
|
+
value: number;
|
|
11
|
+
deviation: number | null;
|
|
12
|
+
deviationUnit: ReturnType<InsightsDisplayDeviationUnits[K]>;
|
|
13
|
+
unit: ReturnType<InsightsDisplayUnits[K]>;
|
|
14
|
+
};
|
|
15
|
+
}[keyof InsightsDisplayUnits]
|
|
16
|
+
>;
|