mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 10:02:05 +00:00
feat(editor): Insights summary banner (#13424)
Co-authored-by: Guillaume Jacquart <jacquart.guillaume@gmail.com>
This commit is contained in:
@@ -23,4 +23,5 @@ export const RESOURCES = {
|
|||||||
workersView: ['manage'] as const,
|
workersView: ['manage'] as const,
|
||||||
workflow: ['share', 'execute', 'move', ...DEFAULT_OPERATIONS] as const,
|
workflow: ['share', 'execute', 'move', ...DEFAULT_OPERATIONS] as const,
|
||||||
folder: [...DEFAULT_OPERATIONS] as const,
|
folder: [...DEFAULT_OPERATIONS] as const,
|
||||||
|
insights: ['list'] as const,
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ interface LoadingProps {
|
|||||||
animated?: boolean;
|
animated?: boolean;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
rows?: number;
|
rows?: number;
|
||||||
|
cols?: number;
|
||||||
shrinkLast?: boolean;
|
shrinkLast?: boolean;
|
||||||
variant?: (typeof VARIANT)[number];
|
variant?: (typeof VARIANT)[number];
|
||||||
}
|
}
|
||||||
@@ -27,6 +28,7 @@ withDefaults(defineProps<LoadingProps>(), {
|
|||||||
animated: true,
|
animated: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
rows: 1,
|
rows: 1,
|
||||||
|
cols: 0,
|
||||||
shrinkLast: true,
|
shrinkLast: true,
|
||||||
variant: 'p',
|
variant: 'p',
|
||||||
});
|
});
|
||||||
@@ -38,7 +40,10 @@ withDefaults(defineProps<LoadingProps>(), {
|
|||||||
:animated="animated"
|
:animated="animated"
|
||||||
:class="['n8n-loading', `n8n-loading-${variant}`]"
|
:class="['n8n-loading', `n8n-loading-${variant}`]"
|
||||||
>
|
>
|
||||||
<template #template>
|
<template v-if="cols" #template>
|
||||||
|
<ElSkeletonItem v-for="i in cols" :key="i" />
|
||||||
|
</template>
|
||||||
|
<template v-else #template>
|
||||||
<div v-if="variant === 'h1'">
|
<div v-if="variant === 'h1'">
|
||||||
<div
|
<div
|
||||||
v-for="(item, index) in rows"
|
v-for="(item, index) in rows"
|
||||||
|
|||||||
@@ -61,21 +61,6 @@ describe('ProjectHeader', () => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render the correct icon', async () => {
|
|
||||||
const { container, rerender } = renderComponent();
|
|
||||||
|
|
||||||
expect(container.querySelector('.fa-home')).toBeVisible();
|
|
||||||
|
|
||||||
projectsStore.currentProject = { type: ProjectTypes.Personal } as Project;
|
|
||||||
await rerender({});
|
|
||||||
expect(container.querySelector('.fa-user')).toBeVisible();
|
|
||||||
|
|
||||||
const projectName = 'My Project';
|
|
||||||
projectsStore.currentProject = { name: projectName } as Project;
|
|
||||||
await rerender({});
|
|
||||||
expect(container.querySelector('.fa-layer-group')).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render the correct title and subtitle', async () => {
|
it('should render the correct title and subtitle', async () => {
|
||||||
const { getByText, queryByText, rerender } = renderComponent();
|
const { getByText, queryByText, rerender } = renderComponent();
|
||||||
const subtitle = 'All the workflows, credentials and executions you have access to';
|
const subtitle = 'All the workflows, credentials and executions you have access to';
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useRoute, useRouter } from 'vue-router';
|
|||||||
import type { UserAction } from '@n8n/design-system';
|
import type { UserAction } from '@n8n/design-system';
|
||||||
import { N8nButton, N8nTooltip } from '@n8n/design-system';
|
import { N8nButton, N8nTooltip } from '@n8n/design-system';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { type ProjectIcon, ProjectTypes } from '@/types/projects.types';
|
import { ProjectTypes } from '@/types/projects.types';
|
||||||
import { useProjectsStore } from '@/stores/projects.store';
|
import { useProjectsStore } from '@/stores/projects.store';
|
||||||
import ProjectTabs from '@/components/Projects/ProjectTabs.vue';
|
import ProjectTabs from '@/components/Projects/ProjectTabs.vue';
|
||||||
import { getResourcePermissions } from '@/permissions';
|
import { getResourcePermissions } from '@/permissions';
|
||||||
@@ -24,16 +24,6 @@ const emit = defineEmits<{
|
|||||||
createFolder: [];
|
createFolder: [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const headerIcon = computed((): ProjectIcon => {
|
|
||||||
if (projectsStore.currentProject?.type === ProjectTypes.Personal) {
|
|
||||||
return { type: 'icon', value: 'user' };
|
|
||||||
} else if (projectsStore.currentProject?.name) {
|
|
||||||
return projectsStore.currentProject.icon ?? { type: 'icon', value: 'layer-group' };
|
|
||||||
} else {
|
|
||||||
return { type: 'icon', value: 'home' };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const projectName = computed(() => {
|
const projectName = computed(() => {
|
||||||
if (!projectsStore.currentProject) {
|
if (!projectsStore.currentProject) {
|
||||||
return i18n.baseText('projects.menu.overview');
|
return i18n.baseText('projects.menu.overview');
|
||||||
@@ -136,7 +126,6 @@ const onSelect = (action: string) => {
|
|||||||
<div>
|
<div>
|
||||||
<div :class="$style.projectHeader">
|
<div :class="$style.projectHeader">
|
||||||
<div :class="$style.projectDetails">
|
<div :class="$style.projectDetails">
|
||||||
<ProjectIcon :icon="headerIcon" :border-less="true" size="medium" />
|
|
||||||
<div :class="$style.headerActions">
|
<div :class="$style.headerActions">
|
||||||
<N8nHeading bold tag="h2" size="xlarge">{{ projectName }}</N8nHeading>
|
<N8nHeading bold tag="h2" size="xlarge">{{ projectName }}</N8nHeading>
|
||||||
<N8nText color="text-light">
|
<N8nText color="text-light">
|
||||||
@@ -168,6 +157,7 @@ const onSelect = (action: string) => {
|
|||||||
</N8nTooltip>
|
</N8nTooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<slot></slot>
|
||||||
<div :class="$style.actions">
|
<div :class="$style.actions">
|
||||||
<ProjectTabs :show-settings="showSettings" />
|
<ProjectTabs :show-settings="showSettings" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
import ConcurrentExecutionsHeader from '@/components/executions/ConcurrentExecutionsHeader.vue';
|
import ConcurrentExecutionsHeader from '@/components/executions/ConcurrentExecutionsHeader.vue';
|
||||||
import ExecutionsFilter from '@/components/executions/ExecutionsFilter.vue';
|
import ExecutionsFilter from '@/components/executions/ExecutionsFilter.vue';
|
||||||
import GlobalExecutionsListItem from '@/components/executions/global/GlobalExecutionsListItem.vue';
|
import GlobalExecutionsListItem from '@/components/executions/global/GlobalExecutionsListItem.vue';
|
||||||
import ProjectHeader from '@/components/Projects/ProjectHeader.vue';
|
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { useMessage } from '@/composables/useMessage';
|
import { useMessage } from '@/composables/useMessage';
|
||||||
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
|
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
|
||||||
@@ -334,7 +333,7 @@ const goToUpgrade = () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="$style.execListWrapper">
|
<div :class="$style.execListWrapper">
|
||||||
<ProjectHeader />
|
<slot />
|
||||||
<div :class="$style.execListHeaderControls">
|
<div :class="$style.execListHeaderControls">
|
||||||
<ExecutionsFilter
|
<ExecutionsFilter
|
||||||
:workflows="workflows"
|
:workflows="workflows"
|
||||||
|
|||||||
20
packages/frontend/editor-ui/src/composables/useOverview.ts
Normal file
20
packages/frontend/editor-ui/src/composables/useOverview.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { computed, reactive } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import { VIEWS } from '@/constants';
|
||||||
|
|
||||||
|
export const useOverview = () => {
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const isOverviewSubPage = computed(
|
||||||
|
() =>
|
||||||
|
route.name === VIEWS.WORKFLOWS ||
|
||||||
|
route.name === VIEWS.HOMEPAGE ||
|
||||||
|
route.name === VIEWS.CREDENTIALS ||
|
||||||
|
route.name === VIEWS.EXECUTIONS ||
|
||||||
|
route.name === VIEWS.FOLDERS,
|
||||||
|
);
|
||||||
|
|
||||||
|
return reactive({
|
||||||
|
isOverviewSubPage,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import InsightsSummary from '@/features/insights/components/InsightsSummary.vue';
|
||||||
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
|
import type { InsightsSummaryDisplay } from '@/features/insights/insights.types';
|
||||||
|
|
||||||
|
const renderComponent = createComponentRenderer(InsightsSummary);
|
||||||
|
|
||||||
|
describe('InsightsSummary', () => {
|
||||||
|
it('should render without error', () => {
|
||||||
|
expect(() =>
|
||||||
|
renderComponent({
|
||||||
|
props: {
|
||||||
|
summary: [],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each<InsightsSummaryDisplay[]>([
|
||||||
|
[[]],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
{ id: 'total', value: 525, deviation: 85, unit: '' },
|
||||||
|
{ id: 'failed', value: 14, deviation: 3, unit: '' },
|
||||||
|
{ id: 'failureRate', value: 1.9, deviation: -0.8, unit: '%' },
|
||||||
|
{ id: 'timeSaved', value: 55.55555555555556, deviation: -5.164722222222222, unit: 'h' },
|
||||||
|
{ id: 'averageRunTime', value: 2.5, deviation: -0.5, unit: 's' },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
{ id: 'total', value: 525, deviation: 85, unit: '' },
|
||||||
|
{ id: 'failed', value: 14, deviation: 3, unit: '' },
|
||||||
|
{ id: 'failureRate', value: 1.9, deviation: -0.8, unit: '%' },
|
||||||
|
{ id: 'timeSaved', value: 0, deviation: 0, unit: 'h' },
|
||||||
|
{ id: 'averageRunTime', value: 2.5, deviation: -0.5, unit: 's' },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
{ id: 'total', value: 525, deviation: -2, unit: '' },
|
||||||
|
{ id: 'failed', value: 14, deviation: -3, unit: '' },
|
||||||
|
{ id: 'failureRate', value: 1.9, deviation: 0.8, unit: '%' },
|
||||||
|
{ id: 'timeSaved', value: 55.55555555555556, deviation: 0, unit: 'h' },
|
||||||
|
{ id: 'averageRunTime', value: 2.5, deviation: 0.5, unit: 's' },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
])('should render the summary correctly', (summary) => {
|
||||||
|
const { html } = renderComponent({
|
||||||
|
props: {
|
||||||
|
summary,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(html()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, useCssModule } from 'vue';
|
||||||
|
import { smartDecimal } from '@n8n/utils/number/smartDecimal';
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
import type { InsightsSummary } from '@n8n/api-types';
|
||||||
|
import type { InsightsSummaryDisplay } from '@/features/insights/insights.types';
|
||||||
|
import {
|
||||||
|
INSIGHT_IMPACT_TYPES,
|
||||||
|
INSIGHTS_UNIT_IMPACT_MAPPING,
|
||||||
|
} from '@/features/insights/insights.constants';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
summary: InsightsSummaryDisplay;
|
||||||
|
loading?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const i18n = useI18n();
|
||||||
|
const $style = useCssModule();
|
||||||
|
|
||||||
|
const summaryTitles = computed<Record<keyof InsightsSummary, string>>(() => ({
|
||||||
|
total: i18n.baseText('insights.banner.title.total'),
|
||||||
|
failed: i18n.baseText('insights.banner.title.failed'),
|
||||||
|
failureRate: i18n.baseText('insights.banner.title.failureRate'),
|
||||||
|
timeSaved: i18n.baseText('insights.banner.title.timeSaved'),
|
||||||
|
averageRunTime: i18n.baseText('insights.banner.title.averageRunTime'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const getSign = (n: number) => (n > 0 ? '+' : undefined);
|
||||||
|
const getImpactStyle = (id: keyof InsightsSummary, value: number) => {
|
||||||
|
const impact = INSIGHTS_UNIT_IMPACT_MAPPING[id];
|
||||||
|
if (value === 0 || impact === INSIGHT_IMPACT_TYPES.NEUTRAL) {
|
||||||
|
return $style.neutral;
|
||||||
|
}
|
||||||
|
if (impact === INSIGHT_IMPACT_TYPES.POSITIVE) {
|
||||||
|
return value > 0 ? $style.positive : $style.negative;
|
||||||
|
}
|
||||||
|
if (impact === INSIGHT_IMPACT_TYPES.NEGATIVE) {
|
||||||
|
return value < 0 ? $style.positive : $style.negative;
|
||||||
|
}
|
||||||
|
return $style.neutral;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="summary.length" :class="$style.insights">
|
||||||
|
<N8nHeading bold tag="h3" size="small" color="text-light" class="mb-xs">{{
|
||||||
|
i18n.baseText('insights.banner.title', { interpolate: { count: 7 } })
|
||||||
|
}}</N8nHeading>
|
||||||
|
<N8nLoading v-if="loading" :class="$style.loading" :cols="5" />
|
||||||
|
<ul v-else data-test-id="insights-summary-tabs">
|
||||||
|
<li
|
||||||
|
v-for="{ id, value, deviation, unit } in summary"
|
||||||
|
:key="id"
|
||||||
|
:data-test-id="`insights-summary-tab-${id}`"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
<strong>{{ summaryTitles[id] }}</strong>
|
||||||
|
<span v-if="value === 0 && id === 'timeSaved'" :class="$style.empty">
|
||||||
|
<em>--</em>
|
||||||
|
<small>
|
||||||
|
<N8nTooltip placement="bottom">
|
||||||
|
<template #content>
|
||||||
|
<i18n-t keypath="insights.banner.timeSaved.tooltip">
|
||||||
|
<template #link>
|
||||||
|
<a href="#">{{
|
||||||
|
i18n.baseText('insights.banner.timeSaved.tooltip.link.text')
|
||||||
|
}}</a>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</template>
|
||||||
|
<N8nIcon :class="$style.icon" icon="info-circle" />
|
||||||
|
</N8nTooltip>
|
||||||
|
</small>
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
<em
|
||||||
|
>{{ smartDecimal(value) }} <i>{{ unit }}</i></em
|
||||||
|
>
|
||||||
|
<small :class="getImpactStyle(id, deviation)">
|
||||||
|
<N8nIcon
|
||||||
|
:class="[$style.icon, getImpactStyle(id, deviation)]"
|
||||||
|
:icon="deviation === 0 ? 'caret-right' : deviation > 0 ? 'caret-up' : 'caret-down'"
|
||||||
|
/>
|
||||||
|
{{ getSign(deviation) }}{{ smartDecimal(deviation) }}
|
||||||
|
</small>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.insights {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
padding: var(--spacing-xs) 0 var(--spacing-2xl);
|
||||||
|
|
||||||
|
ul {
|
||||||
|
display: flex;
|
||||||
|
height: 91px;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: flex-start;
|
||||||
|
border: var(--border-width-base) var(--border-style-base) var(--color-foreground-base);
|
||||||
|
border-radius: 6px;
|
||||||
|
list-style: none;
|
||||||
|
background-color: var(--color-background-xlight);
|
||||||
|
overflow-x: auto;
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
border-left: var(--border-width-base) var(--border-style-base) var(--color-foreground-base);
|
||||||
|
padding: 0 var(--spacing-xl) 0 var(--spacing-l);
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
border-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
display: grid;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
color: var(--color-text-dark);
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
font-weight: 400;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-bottom: var(--spacing-2xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
|
||||||
|
&.empty {
|
||||||
|
em {
|
||||||
|
color: var(--color-text-lighter);
|
||||||
|
}
|
||||||
|
small {
|
||||||
|
padding: 0;
|
||||||
|
height: 21px;
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
height: 20px;
|
||||||
|
width: 8px;
|
||||||
|
top: -3px;
|
||||||
|
transform: translateY(0);
|
||||||
|
color: var(--color-text-light);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
em {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: flex-start;
|
||||||
|
color: var(--color-text-dark);
|
||||||
|
font-size: var(--font-size-2xl);
|
||||||
|
line-height: 100%;
|
||||||
|
font-weight: 600;
|
||||||
|
font-style: normal;
|
||||||
|
gap: var(--spacing-5xs);
|
||||||
|
|
||||||
|
i {
|
||||||
|
color: var(--color-text-light);
|
||||||
|
font-size: 22px;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 0 0 18px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.positive {
|
||||||
|
color: var(--color-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.negative {
|
||||||
|
color: var(--color-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.neutral {
|
||||||
|
color: var(--color-text-light);
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
font-size: 23px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 32px;
|
||||||
|
left: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
display: flex;
|
||||||
|
align-self: stretch;
|
||||||
|
align-items: stretch;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
margin: 0;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`InsightsSummary > should render the summary correctly 1`] = `"<!--v-if-->"`;
|
||||||
|
|
||||||
|
exports[`InsightsSummary > should render the summary correctly 2`] = `
|
||||||
|
"<div class="insights">
|
||||||
|
<h3 class="n8n-heading text-light size-small bold mb-xs mb-xs">Production executions for the last 7 days</h3>
|
||||||
|
<ul data-test-id="insights-summary-tabs">
|
||||||
|
<li data-test-id="insights-summary-tab-total">
|
||||||
|
<p><strong>Total</strong><span><em>525 <i></i></em><small class="positive"><span class="n8n-text compact size-medium regular n8n-icon icon positive icon positive n8n-icon icon positive icon positive"><svg class="svg-inline--fa fa-caret-up fa-w-10 medium" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="caret-up" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path class="" fill="currentColor" d="M288.662 352H31.338c-17.818 0-26.741-21.543-14.142-34.142l128.662-128.662c7.81-7.81 20.474-7.81 28.284 0l128.662 128.662c12.6 12.599 3.676 34.142-14.142 34.142z"></path></svg></span> +85</small></span></p>
|
||||||
|
</li>
|
||||||
|
<li data-test-id="insights-summary-tab-failed">
|
||||||
|
<p><strong>Failed</strong><span><em>14 <i></i></em><small class="negative"><span class="n8n-text compact size-medium regular n8n-icon icon negative icon negative n8n-icon icon negative icon negative"><svg class="svg-inline--fa fa-caret-up fa-w-10 medium" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="caret-up" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path class="" fill="currentColor" d="M288.662 352H31.338c-17.818 0-26.741-21.543-14.142-34.142l128.662-128.662c7.81-7.81 20.474-7.81 28.284 0l128.662 128.662c12.6 12.599 3.676 34.142-14.142 34.142z"></path></svg></span> +3</small></span></p>
|
||||||
|
</li>
|
||||||
|
<li data-test-id="insights-summary-tab-failureRate">
|
||||||
|
<p><strong>Failure rate</strong><span><em>1.9 <i>%</i></em><small class="positive"><span class="n8n-text compact size-medium regular n8n-icon icon positive icon positive n8n-icon icon positive icon positive"><svg class="svg-inline--fa fa-caret-down fa-w-10 medium" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="caret-down" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path class="" fill="currentColor" d="M31.3 192h257.3c17.8 0 26.7 21.5 14.1 34.1L174.1 354.8c-7.8 7.8-20.5 7.8-28.3 0L17.2 226.1C4.6 213.5 13.5 192 31.3 192z"></path></svg></span> -0.8</small></span></p>
|
||||||
|
</li>
|
||||||
|
<li data-test-id="insights-summary-tab-timeSaved">
|
||||||
|
<p><strong>Time saved</strong><span><em>55.56 <i>h</i></em><small class="negative"><span class="n8n-text compact size-medium regular n8n-icon icon negative icon negative n8n-icon icon negative icon negative"><svg class="svg-inline--fa fa-caret-down fa-w-10 medium" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="caret-down" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path class="" fill="currentColor" d="M31.3 192h257.3c17.8 0 26.7 21.5 14.1 34.1L174.1 354.8c-7.8 7.8-20.5 7.8-28.3 0L17.2 226.1C4.6 213.5 13.5 192 31.3 192z"></path></svg></span> -5.16</small></span></p>
|
||||||
|
</li>
|
||||||
|
<li data-test-id="insights-summary-tab-averageRunTime">
|
||||||
|
<p><strong>Avg. run time</strong><span><em>2.5 <i>s</i></em><small class="neutral"><span class="n8n-text compact size-medium regular n8n-icon icon neutral icon neutral n8n-icon icon neutral icon neutral"><svg class="svg-inline--fa fa-caret-down fa-w-10 medium" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="caret-down" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path class="" fill="currentColor" d="M31.3 192h257.3c17.8 0 26.7 21.5 14.1 34.1L174.1 354.8c-7.8 7.8-20.5 7.8-28.3 0L17.2 226.1C4.6 213.5 13.5 192 31.3 192z"></path></svg></span> -0.5</small></span></p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`InsightsSummary > should render the summary correctly 3`] = `
|
||||||
|
"<div class="insights">
|
||||||
|
<h3 class="n8n-heading text-light size-small bold mb-xs mb-xs">Production executions for the last 7 days</h3>
|
||||||
|
<ul data-test-id="insights-summary-tabs">
|
||||||
|
<li data-test-id="insights-summary-tab-total">
|
||||||
|
<p><strong>Total</strong><span><em>525 <i></i></em><small class="positive"><span class="n8n-text compact size-medium regular n8n-icon icon positive icon positive n8n-icon icon positive icon positive"><svg class="svg-inline--fa fa-caret-up fa-w-10 medium" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="caret-up" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path class="" fill="currentColor" d="M288.662 352H31.338c-17.818 0-26.741-21.543-14.142-34.142l128.662-128.662c7.81-7.81 20.474-7.81 28.284 0l128.662 128.662c12.6 12.599 3.676 34.142-14.142 34.142z"></path></svg></span> +85</small></span></p>
|
||||||
|
</li>
|
||||||
|
<li data-test-id="insights-summary-tab-failed">
|
||||||
|
<p><strong>Failed</strong><span><em>14 <i></i></em><small class="negative"><span class="n8n-text compact size-medium regular n8n-icon icon negative icon negative n8n-icon icon negative icon negative"><svg class="svg-inline--fa fa-caret-up fa-w-10 medium" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="caret-up" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path class="" fill="currentColor" d="M288.662 352H31.338c-17.818 0-26.741-21.543-14.142-34.142l128.662-128.662c7.81-7.81 20.474-7.81 28.284 0l128.662 128.662c12.6 12.599 3.676 34.142-14.142 34.142z"></path></svg></span> +3</small></span></p>
|
||||||
|
</li>
|
||||||
|
<li data-test-id="insights-summary-tab-failureRate">
|
||||||
|
<p><strong>Failure rate</strong><span><em>1.9 <i>%</i></em><small class="positive"><span class="n8n-text compact size-medium regular n8n-icon icon positive icon positive n8n-icon icon positive icon positive"><svg class="svg-inline--fa fa-caret-down fa-w-10 medium" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="caret-down" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path class="" fill="currentColor" d="M31.3 192h257.3c17.8 0 26.7 21.5 14.1 34.1L174.1 354.8c-7.8 7.8-20.5 7.8-28.3 0L17.2 226.1C4.6 213.5 13.5 192 31.3 192z"></path></svg></span> -0.8</small></span></p>
|
||||||
|
</li>
|
||||||
|
<li data-test-id="insights-summary-tab-timeSaved">
|
||||||
|
<p><strong>Time saved</strong><span class="empty"><em>--</em><small><span class="n8n-text compact size-medium regular n8n-icon icon el-tooltip__trigger el-tooltip__trigger icon el-tooltip__trigger el-tooltip__trigger n8n-icon icon el-tooltip__trigger el-tooltip__trigger icon el-tooltip__trigger el-tooltip__trigger"><svg class="svg-inline--fa fa-info-circle fa-w-16 medium" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="info-circle" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path class="" fill="currentColor" d="M256 8C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm0 110c23.196 0 42 18.804 42 42s-18.804 42-42 42-42-18.804-42-42 18.804-42 42-42zm56 254c0 6.627-5.373 12-12 12h-88c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h12v-64h-12c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h64c6.627 0 12 5.373 12 12v100h12c6.627 0 12 5.373 12 12v24z"></path></svg></span>
|
||||||
|
<!--teleport start-->
|
||||||
|
<!--teleport end--></small></span>
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
<li data-test-id="insights-summary-tab-averageRunTime">
|
||||||
|
<p><strong>Avg. run time</strong><span><em>2.5 <i>s</i></em><small class="neutral"><span class="n8n-text compact size-medium regular n8n-icon icon neutral icon neutral n8n-icon icon neutral icon neutral"><svg class="svg-inline--fa fa-caret-down fa-w-10 medium" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="caret-down" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path class="" fill="currentColor" d="M31.3 192h257.3c17.8 0 26.7 21.5 14.1 34.1L174.1 354.8c-7.8 7.8-20.5 7.8-28.3 0L17.2 226.1C4.6 213.5 13.5 192 31.3 192z"></path></svg></span> -0.5</small></span></p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`InsightsSummary > should render the summary correctly 4`] = `
|
||||||
|
"<div class="insights">
|
||||||
|
<h3 class="n8n-heading text-light size-small bold mb-xs mb-xs">Production executions for the last 7 days</h3>
|
||||||
|
<ul data-test-id="insights-summary-tabs">
|
||||||
|
<li data-test-id="insights-summary-tab-total">
|
||||||
|
<p><strong>Total</strong><span><em>525 <i></i></em><small class="negative"><span class="n8n-text compact size-medium regular n8n-icon icon negative icon negative n8n-icon icon negative icon negative"><svg class="svg-inline--fa fa-caret-down fa-w-10 medium" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="caret-down" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path class="" fill="currentColor" d="M31.3 192h257.3c17.8 0 26.7 21.5 14.1 34.1L174.1 354.8c-7.8 7.8-20.5 7.8-28.3 0L17.2 226.1C4.6 213.5 13.5 192 31.3 192z"></path></svg></span> -2</small></span></p>
|
||||||
|
</li>
|
||||||
|
<li data-test-id="insights-summary-tab-failed">
|
||||||
|
<p><strong>Failed</strong><span><em>14 <i></i></em><small class="positive"><span class="n8n-text compact size-medium regular n8n-icon icon positive icon positive n8n-icon icon positive icon positive"><svg class="svg-inline--fa fa-caret-down fa-w-10 medium" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="caret-down" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path class="" fill="currentColor" d="M31.3 192h257.3c17.8 0 26.7 21.5 14.1 34.1L174.1 354.8c-7.8 7.8-20.5 7.8-28.3 0L17.2 226.1C4.6 213.5 13.5 192 31.3 192z"></path></svg></span> -3</small></span></p>
|
||||||
|
</li>
|
||||||
|
<li data-test-id="insights-summary-tab-failureRate">
|
||||||
|
<p><strong>Failure rate</strong><span><em>1.9 <i>%</i></em><small class="negative"><span class="n8n-text compact size-medium regular n8n-icon icon negative icon negative n8n-icon icon negative icon negative"><svg class="svg-inline--fa fa-caret-up fa-w-10 medium" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="caret-up" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path class="" fill="currentColor" d="M288.662 352H31.338c-17.818 0-26.741-21.543-14.142-34.142l128.662-128.662c7.81-7.81 20.474-7.81 28.284 0l128.662 128.662c12.6 12.599 3.676 34.142-14.142 34.142z"></path></svg></span> +0.8</small></span></p>
|
||||||
|
</li>
|
||||||
|
<li data-test-id="insights-summary-tab-timeSaved">
|
||||||
|
<p><strong>Time saved</strong><span><em>55.56 <i>h</i></em><small class="neutral"><span class="n8n-text compact size-medium regular n8n-icon icon neutral icon neutral n8n-icon icon neutral icon neutral"><svg class="svg-inline--fa fa-caret-right fa-w-6 medium" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="caret-right" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 192 512"><path class="" fill="currentColor" d="M0 384.662V127.338c0-17.818 21.543-26.741 34.142-14.142l128.662 128.662c7.81 7.81 7.81 20.474 0 28.284L34.142 398.804C21.543 411.404 0 402.48 0 384.662z"></path></svg></span> 0</small></span></p>
|
||||||
|
</li>
|
||||||
|
<li data-test-id="insights-summary-tab-averageRunTime">
|
||||||
|
<p><strong>Avg. run time</strong><span><em>2.5 <i>s</i></em><small class="neutral"><span class="n8n-text compact size-medium regular n8n-icon icon neutral icon neutral n8n-icon icon neutral icon neutral"><svg class="svg-inline--fa fa-caret-up fa-w-10 medium" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="caret-up" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path class="" fill="currentColor" d="M288.662 352H31.338c-17.818 0-26.741-21.543-14.142-34.142l128.662-128.662c7.81-7.81 20.474-7.81 28.284 0l128.662 128.662c12.6 12.599 3.676 34.142-14.142 34.142z"></path></svg></span> +0.5</small></span></p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>"
|
||||||
|
`;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import type { IRestApiContext } from '@/Interface';
|
||||||
|
import { makeRestApiRequest } from '@/utils/apiUtils';
|
||||||
|
import type { InsightsSummary } from '@n8n/api-types';
|
||||||
|
|
||||||
|
export const fetchInsightsSummary = async (context: IRestApiContext): Promise<InsightsSummary> =>
|
||||||
|
await makeRestApiRequest<InsightsSummary>(context, 'GET', '/insights/summary');
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import type { InsightsSummaryType } from '@n8n/api-types';
|
||||||
|
|
||||||
|
export const INSIGHTS_SUMMARY_ORDER: InsightsSummaryType[] = [
|
||||||
|
'total',
|
||||||
|
'failed',
|
||||||
|
'failureRate',
|
||||||
|
'timeSaved',
|
||||||
|
'averageRunTime',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const INSIGHTS_UNIT_MAPPING: Record<InsightsSummaryType, string> = {
|
||||||
|
total: '',
|
||||||
|
failed: '',
|
||||||
|
failureRate: '%',
|
||||||
|
timeSaved: 'h',
|
||||||
|
averageRunTime: 's',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const INSIGHT_IMPACT_TYPES = {
|
||||||
|
POSITIVE: 'positive',
|
||||||
|
NEGATIVE: 'negative',
|
||||||
|
NEUTRAL: 'neutral',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const INSIGHTS_UNIT_IMPACT_MAPPING: Record<
|
||||||
|
InsightsSummaryType,
|
||||||
|
(typeof INSIGHT_IMPACT_TYPES)[keyof typeof INSIGHT_IMPACT_TYPES]
|
||||||
|
> = {
|
||||||
|
total: INSIGHT_IMPACT_TYPES.POSITIVE,
|
||||||
|
failed: INSIGHT_IMPACT_TYPES.NEGATIVE,
|
||||||
|
failureRate: INSIGHT_IMPACT_TYPES.NEGATIVE, // Higher failureRate is bad → negative (red)
|
||||||
|
timeSaved: INSIGHT_IMPACT_TYPES.POSITIVE, // More time saved is good → positive (green)
|
||||||
|
averageRunTime: INSIGHT_IMPACT_TYPES.NEUTRAL, // Not good or bad → neutral (grey)
|
||||||
|
} as const;
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { computed } from 'vue';
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { useAsyncState } from '@vueuse/core';
|
||||||
|
import * as insightsApi from '@/features/insights/insights.api';
|
||||||
|
import { useRootStore } from '@/stores/root.store';
|
||||||
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
|
import { transformInsightsSummary } from '@/features/insights/insights.utils';
|
||||||
|
import { getResourcePermissions } from '@/permissions';
|
||||||
|
|
||||||
|
export const useInsightsStore = defineStore('insights', () => {
|
||||||
|
const rootStore = useRootStore();
|
||||||
|
const usersStore = useUsersStore();
|
||||||
|
|
||||||
|
const globalInsightsPermissions = computed(
|
||||||
|
() => getResourcePermissions(usersStore.currentUser?.globalScopes).insights,
|
||||||
|
);
|
||||||
|
|
||||||
|
const summary = useAsyncState(
|
||||||
|
async () => {
|
||||||
|
if (!globalInsightsPermissions.value.list) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = await insightsApi.fetchInsightsSummary(rootStore.restApiContext);
|
||||||
|
return transformInsightsSummary(raw);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
{ immediate: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
summary,
|
||||||
|
globalInsightsPermissions,
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import type { INSIGHTS_UNIT_MAPPING } from '@/features/insights/insights.constants';
|
||||||
|
|
||||||
|
type InsightsDisplayUnits = typeof INSIGHTS_UNIT_MAPPING;
|
||||||
|
|
||||||
|
export type InsightsSummaryDisplay = Array<
|
||||||
|
{
|
||||||
|
[K in keyof InsightsDisplayUnits]: {
|
||||||
|
id: K;
|
||||||
|
value: number;
|
||||||
|
deviation: number;
|
||||||
|
unit: InsightsDisplayUnits[K];
|
||||||
|
};
|
||||||
|
}[keyof InsightsDisplayUnits]
|
||||||
|
>;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import type { InsightsSummary, InsightsSummaryType } from '@n8n/api-types';
|
||||||
|
import type { InsightsSummaryDisplay } from '@/features/insights/insights.types';
|
||||||
|
import {
|
||||||
|
INSIGHTS_SUMMARY_ORDER,
|
||||||
|
INSIGHTS_UNIT_MAPPING,
|
||||||
|
} from '@/features/insights/insights.constants';
|
||||||
|
|
||||||
|
const transformInsightsValues: Partial<Record<InsightsSummaryType, (value: number) => number>> = {
|
||||||
|
timeSaved: (value: number): number => value / 3600, // we want to show saved time in hours
|
||||||
|
averageRunTime: (value: number): number => Math.round((value / 1000) * 100) / 100, // we want to show average run time in seconds with 2 decimal places
|
||||||
|
failureRate: (value: number): number => value * 100, // we want to show failure rate in percentage
|
||||||
|
};
|
||||||
|
|
||||||
|
export const transformInsightsSummary = (data: InsightsSummary | null): InsightsSummaryDisplay =>
|
||||||
|
data
|
||||||
|
? INSIGHTS_SUMMARY_ORDER.map((key) => ({
|
||||||
|
id: key,
|
||||||
|
value: transformInsightsValues[key]?.(data[key].value) ?? data[key].value,
|
||||||
|
deviation: transformInsightsValues[key]?.(data[key].deviation) ?? data[key].deviation,
|
||||||
|
unit: INSIGHTS_UNIT_MAPPING[key],
|
||||||
|
}))
|
||||||
|
: [];
|
||||||
@@ -9,6 +9,7 @@ import { useExternalHooks } from '@/composables/useExternalHooks';
|
|||||||
import { useVersionsStore } from '@/stores/versions.store';
|
import { useVersionsStore } from '@/stores/versions.store';
|
||||||
import { useProjectsStore } from '@/stores/projects.store';
|
import { useProjectsStore } from '@/stores/projects.store';
|
||||||
import { useRolesStore } from './stores/roles.store';
|
import { useRolesStore } from './stores/roles.store';
|
||||||
|
import { useInsightsStore } from '@/features/insights/insights.store';
|
||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import SourceControlInitializationErrorMessage from '@/components/SourceControlInitializationErrorMessage.vue';
|
import SourceControlInitializationErrorMessage from '@/components/SourceControlInitializationErrorMessage.vue';
|
||||||
@@ -66,6 +67,7 @@ export async function initializeAuthenticatedFeatures(
|
|||||||
const cloudPlanStore = useCloudPlanStore();
|
const cloudPlanStore = useCloudPlanStore();
|
||||||
const projectsStore = useProjectsStore();
|
const projectsStore = useProjectsStore();
|
||||||
const rolesStore = useRolesStore();
|
const rolesStore = useRolesStore();
|
||||||
|
const insightsStore = useInsightsStore();
|
||||||
|
|
||||||
if (sourceControlStore.isEnterpriseSourceControlEnabled) {
|
if (sourceControlStore.isEnterpriseSourceControlEnabled) {
|
||||||
try {
|
try {
|
||||||
@@ -103,6 +105,7 @@ export async function initializeAuthenticatedFeatures(
|
|||||||
projectsStore.getPersonalProject(),
|
projectsStore.getPersonalProject(),
|
||||||
projectsStore.getProjectsCount(),
|
projectsStore.getProjectsCount(),
|
||||||
rolesStore.fetchRoles(),
|
rolesStore.fetchRoles(),
|
||||||
|
insightsStore.summary.execute(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
authenticatedFeaturesInitialized = true;
|
authenticatedFeaturesInitialized = true;
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ describe('permissions', () => {
|
|||||||
workersView: {},
|
workersView: {},
|
||||||
workflow: {},
|
workflow: {},
|
||||||
folder: {},
|
folder: {},
|
||||||
|
insights: {},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('getResourcePermissions', () => {
|
it('getResourcePermissions', () => {
|
||||||
@@ -59,6 +60,7 @@ describe('permissions', () => {
|
|||||||
'workflow:share',
|
'workflow:share',
|
||||||
'workflow:update',
|
'workflow:update',
|
||||||
'folder:create',
|
'folder:create',
|
||||||
|
'insights:list',
|
||||||
];
|
];
|
||||||
|
|
||||||
const permissionRecord: PermissionsRecord = {
|
const permissionRecord: PermissionsRecord = {
|
||||||
@@ -120,6 +122,9 @@ describe('permissions', () => {
|
|||||||
folder: {
|
folder: {
|
||||||
create: true,
|
create: true,
|
||||||
},
|
},
|
||||||
|
insights: {
|
||||||
|
list: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(getResourcePermissions(scopes)).toEqual(permissionRecord);
|
expect(getResourcePermissions(scopes)).toEqual(permissionRecord);
|
||||||
|
|||||||
@@ -3067,5 +3067,13 @@
|
|||||||
"freeAi.credits.showError.claim.title": "Free AI credits",
|
"freeAi.credits.showError.claim.title": "Free AI credits",
|
||||||
"freeAi.credits.showError.claim.message": "Enable to claim credits",
|
"freeAi.credits.showError.claim.message": "Enable to claim credits",
|
||||||
"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.banner.title": "Production executions for the last {count} days",
|
||||||
|
"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.title.total": "Total",
|
||||||
|
"insights.banner.title.failed": "Failed",
|
||||||
|
"insights.banner.title.failureRate": "Failure rate",
|
||||||
|
"insights.banner.title.timeSaved": "Time saved",
|
||||||
|
"insights.banner.title.averageRunTime": "Avg. run time"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ import {
|
|||||||
faBrain,
|
faBrain,
|
||||||
faCalculator,
|
faCalculator,
|
||||||
faCalendar,
|
faCalendar,
|
||||||
|
faCaretDown,
|
||||||
|
faCaretRight,
|
||||||
|
faCaretUp,
|
||||||
faChartBar,
|
faChartBar,
|
||||||
faCheck,
|
faCheck,
|
||||||
faCheckCircle,
|
faCheckCircle,
|
||||||
@@ -216,6 +219,9 @@ export const FontAwesomePlugin: Plugin = {
|
|||||||
addIcon(faBrain);
|
addIcon(faBrain);
|
||||||
addIcon(faCalculator);
|
addIcon(faCalculator);
|
||||||
addIcon(faCalendar);
|
addIcon(faCalendar);
|
||||||
|
addIcon(faCaretDown);
|
||||||
|
addIcon(faCaretRight);
|
||||||
|
addIcon(faCaretUp);
|
||||||
addIcon(faChartBar);
|
addIcon(faChartBar);
|
||||||
addIcon(faCheck);
|
addIcon(faCheck);
|
||||||
addIcon(faCheckCircle);
|
addIcon(faCheckCircle);
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export const useRBACStore = defineStore(STORES.RBAC, () => {
|
|||||||
saml: {},
|
saml: {},
|
||||||
securityAudit: {},
|
securityAudit: {},
|
||||||
folder: {},
|
folder: {},
|
||||||
|
insights: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
function addGlobalRole(role: IRole) {
|
function addGlobalRole(role: IRole) {
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ import { N8nCheckbox } from '@n8n/design-system';
|
|||||||
import { pickBy } from 'lodash-es';
|
import { pickBy } from 'lodash-es';
|
||||||
import { CREDENTIAL_EMPTY_VALUE } from 'n8n-workflow';
|
import { CREDENTIAL_EMPTY_VALUE } from 'n8n-workflow';
|
||||||
import { isCredentialsResource } from '@/utils/typeGuards';
|
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';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
credentialId?: string;
|
credentialId?: string;
|
||||||
@@ -44,12 +47,14 @@ const sourceControlStore = useSourceControlStore();
|
|||||||
const externalSecretsStore = useExternalSecretsStore();
|
const externalSecretsStore = useExternalSecretsStore();
|
||||||
const projectsStore = useProjectsStore();
|
const projectsStore = useProjectsStore();
|
||||||
const usersStore = useUsersStore();
|
const usersStore = useUsersStore();
|
||||||
|
const insightsStore = useInsightsStore();
|
||||||
|
|
||||||
const documentTitle = useDocumentTitle();
|
const documentTitle = useDocumentTitle();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
const overview = useOverview();
|
||||||
|
|
||||||
type Filters = BaseFilters & { type?: string[]; setupNeeded?: boolean };
|
type Filters = BaseFilters & { type?: string[]; setupNeeded?: boolean };
|
||||||
const updateFilter = (state: Filters) => {
|
const updateFilter = (state: Filters) => {
|
||||||
@@ -235,7 +240,13 @@ onMounted(() => {
|
|||||||
@update:search="onSearchUpdated"
|
@update:search="onSearchUpdated"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<ProjectHeader />
|
<ProjectHeader>
|
||||||
|
<InsightsSummary
|
||||||
|
v-if="overview.isOverviewSubPage"
|
||||||
|
:loading="insightsStore.summary.isLoading"
|
||||||
|
:summary="insightsStore.summary.state"
|
||||||
|
/>
|
||||||
|
</ProjectHeader>
|
||||||
</template>
|
</template>
|
||||||
<template #default="{ data }">
|
<template #default="{ data }">
|
||||||
<CredentialCard
|
<CredentialCard
|
||||||
|
|||||||
@@ -2,15 +2,19 @@
|
|||||||
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 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 { useI18n } from '@/composables/useI18n';
|
||||||
import { useTelemetry } from '@/composables/useTelemetry';
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
import { useExternalHooks } from '@/composables/useExternalHooks';
|
import { useExternalHooks } from '@/composables/useExternalHooks';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { useExecutionsStore } from '@/stores/executions.store';
|
import { useExecutionsStore } from '@/stores/executions.store';
|
||||||
|
import { useInsightsStore } from '@/features/insights/insights.store';
|
||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import type { ExecutionFilterType } from '@/Interface';
|
import type { ExecutionFilterType } from '@/Interface';
|
||||||
|
import { useOverview } from '@/composables/useOverview';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
@@ -18,8 +22,10 @@ const telemetry = useTelemetry();
|
|||||||
const externalHooks = useExternalHooks();
|
const externalHooks = useExternalHooks();
|
||||||
const workflowsStore = useWorkflowsStore();
|
const workflowsStore = useWorkflowsStore();
|
||||||
const executionsStore = useExecutionsStore();
|
const executionsStore = useExecutionsStore();
|
||||||
|
const insightsStore = useInsightsStore();
|
||||||
const documentTitle = useDocumentTitle();
|
const documentTitle = useDocumentTitle();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
const overview = useOverview();
|
||||||
|
|
||||||
const { executionsCount, executionsCountEstimated, filters, allExecutions } =
|
const { executionsCount, executionsCountEstimated, filters, allExecutions } =
|
||||||
storeToRefs(executionsStore);
|
storeToRefs(executionsStore);
|
||||||
@@ -87,5 +93,13 @@ async function onExecutionStop() {
|
|||||||
:estimated-total="executionsCountEstimated"
|
:estimated-total="executionsCountEstimated"
|
||||||
@execution:stop="onExecutionStop"
|
@execution:stop="onExecutionStop"
|
||||||
@update:filters="onUpdateFilters"
|
@update:filters="onUpdateFilters"
|
||||||
|
>
|
||||||
|
<ProjectHeader>
|
||||||
|
<InsightsSummary
|
||||||
|
v-if="overview.isOverviewSubPage"
|
||||||
|
:loading="insightsStore.summary.isLoading"
|
||||||
|
:summary="insightsStore.summary.state"
|
||||||
/>
|
/>
|
||||||
|
</ProjectHeader>
|
||||||
|
</GlobalExecutionsList>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -61,6 +61,9 @@ import { useToast } from '@/composables/useToast';
|
|||||||
import { useFoldersStore } from '@/stores/folders.store';
|
import { useFoldersStore } from '@/stores/folders.store';
|
||||||
import { useFolders } from '@/composables/useFolders';
|
import { useFolders } from '@/composables/useFolders';
|
||||||
import { useUsageStore } from '@/stores/usage.store';
|
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';
|
||||||
|
|
||||||
const SEARCH_DEBOUNCE_TIME = 300;
|
const SEARCH_DEBOUNCE_TIME = 300;
|
||||||
const FILTERS_DEBOUNCE_TIME = 100;
|
const FILTERS_DEBOUNCE_TIME = 100;
|
||||||
@@ -102,9 +105,11 @@ const uiStore = useUIStore();
|
|||||||
const tagsStore = useTagsStore();
|
const tagsStore = useTagsStore();
|
||||||
const foldersStore = useFoldersStore();
|
const foldersStore = useFoldersStore();
|
||||||
const usageStore = useUsageStore();
|
const usageStore = useUsageStore();
|
||||||
|
const insightsStore = useInsightsStore();
|
||||||
|
|
||||||
const documentTitle = useDocumentTitle();
|
const documentTitle = useDocumentTitle();
|
||||||
const { callDebounced } = useDebounce();
|
const { callDebounced } = useDebounce();
|
||||||
|
const overview = useOverview();
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const breadcrumbsLoading = ref(false);
|
const breadcrumbsLoading = ref(false);
|
||||||
@@ -1213,7 +1218,13 @@ const onCreateWorkflowClick = () => {
|
|||||||
@sort="onSortUpdated"
|
@sort="onSortUpdated"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<ProjectHeader @create-folder="createFolderInCurrent" />
|
<ProjectHeader>
|
||||||
|
<InsightsSummary
|
||||||
|
v-if="overview.isOverviewSubPage"
|
||||||
|
:loading="insightsStore.summary.isLoading"
|
||||||
|
:summary="insightsStore.summary.state"
|
||||||
|
/>
|
||||||
|
</ProjectHeader>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="foldersEnabled" #add-button>
|
<template v-if="foldersEnabled" #add-button>
|
||||||
<N8nTooltip
|
<N8nTooltip
|
||||||
|
|||||||
Reference in New Issue
Block a user