fix(editor): Insights design review 3 (no-changelog) (#14520)

This commit is contained in:
Raúl Gómez Morales
2025-04-11 09:40:38 +02:00
committed by GitHub
parent 5826ff2cec
commit e54f450a9d
6 changed files with 121 additions and 107 deletions

View File

@@ -29,6 +29,7 @@ export const generateLineChartOptions = (
{ {
responsive: true, responsive: true,
maintainAspectRatio: false, maintainAspectRatio: false,
animation: false,
plugins: { plugins: {
legend: { legend: {
display: false, display: false,
@@ -115,6 +116,7 @@ export const generateBarChartOptions = (
{ {
responsive: true, responsive: true,
maintainAspectRatio: false, maintainAspectRatio: false,
animation: false,
plugins: { plugins: {
legend: { legend: {
display: true, display: true,
@@ -126,6 +128,7 @@ export const generateBarChartOptions = (
boxHeight: 8, boxHeight: 8,
borderRadius: 2, borderRadius: 2,
useBorderRadius: true, useBorderRadius: true,
color: colorTextLight.value,
}, },
}, },
tooltip: { tooltip: {

View File

@@ -91,56 +91,64 @@ watch(
<template> <template>
<div :class="$style.insightsView"> <div :class="$style.insightsView">
<N8nHeading bold tag="h2" size="xlarge"> <div :class="$style.insightsContainer">
{{ i18n.baseText('insights.dashboard.title') }} <N8nHeading bold tag="h2" size="xlarge">
</N8nHeading> {{ i18n.baseText('insights.dashboard.title') }}
<div> </N8nHeading>
<InsightsSummary <div>
v-if="insightsStore.isSummaryEnabled" <InsightsSummary
:summary="insightsStore.summary.state" v-if="insightsStore.isSummaryEnabled"
:loading="insightsStore.summary.isLoading" :summary="insightsStore.summary.state"
:class="$style.insightsBanner" :loading="insightsStore.summary.isLoading"
/> :class="$style.insightsBanner"
<div v-if="insightsStore.isInsightsEnabled" :class="$style.insightsContent"> />
<div :class="$style.insightsChartWrapper"> <div v-if="insightsStore.isInsightsEnabled" :class="$style.insightsContent">
<template v-if="insightsStore.charts.isLoading"> loading </template> <div :class="$style.insightsChartWrapper">
<component <template v-if="insightsStore.charts.isLoading"> loading </template>
:is="chartComponents[props.insightType]" <component
v-else :is="chartComponents[props.insightType]"
:type="props.insightType" v-else
:data="insightsStore.charts.state" :type="props.insightType"
/> :data="insightsStore.charts.state"
</div> />
<div :class="$style.insightsTableWrapper"> </div>
<InsightsTableWorkflows <div :class="$style.insightsTableWrapper">
v-model:sort-by="sortTableBy" <InsightsTableWorkflows
:data="insightsStore.table.state" v-model:sort-by="sortTableBy"
:loading="insightsStore.table.isLoading" :data="insightsStore.table.state"
@update:options="fetchPaginatedTableData" :loading="insightsStore.table.isLoading"
/> @update:options="fetchPaginatedTableData"
/>
</div>
</div> </div>
<InsightsPaywall v-else data-test-id="insights-dashboard-unlicensed" />
</div> </div>
<InsightsPaywall v-else data-test-id="insights-dashboard-unlicensed" />
</div> </div>
</div> </div>
</template> </template>
<style lang="scss" module> <style lang="scss" module>
.insightsView { .insightsView {
padding: var(--spacing-l) var(--spacing-2xl);
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 30px; gap: 30px;
overflow: auto; overflow: auto;
}
.insightsContainer {
width: 100%;
max-width: var(--content-container-width); max-width: var(--content-container-width);
padding: var(--spacing-l) var(--spacing-2xl);
margin: 0 auto;
} }
.insightsBanner { .insightsBanner {
padding-bottom: 0; padding-bottom: 0;
ul { ul {
border-radius: 0; border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
} }
} }

View File

@@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from '@/composables/useI18n'; import { useI18n } from '@/composables/useI18n';
import { useTelemetry } from '@/composables/useTelemetry';
import { VIEWS } from '@/constants'; import { VIEWS } from '@/constants';
import { import {
INSIGHT_IMPACT_TYPES, INSIGHT_IMPACT_TYPES,
@@ -10,7 +11,6 @@ import type { InsightsSummary } from '@n8n/api-types';
import { smartDecimal } from '@n8n/utils/number/smartDecimal'; import { smartDecimal } from '@n8n/utils/number/smartDecimal';
import { computed, ref, useCssModule } from 'vue'; import { computed, ref, useCssModule } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useTelemetry } from '@/composables/useTelemetry';
const props = defineProps<{ const props = defineProps<{
summary: InsightsSummaryDisplay; summary: InsightsSummaryDisplay;
@@ -62,61 +62,65 @@ const trackTabClick = (insightType: keyof InsightsSummary) => {
<template> <template>
<div :class="$style.insights"> <div :class="$style.insights">
<N8nLoading v-if="loading" :class="$style.loading" :cols="5" /> <ul data-test-id="insights-summary-tabs">
<ul v-else data-test-id="insights-summary-tabs"> <N8nLoading v-if="loading" :class="$style.loading" :cols="5" />
<li <template v-else>
v-for="{ id, value, deviation, deviationUnit, unit, to } in summaryWithRouteLocations" <li
:key="id" v-for="{ id, value, deviation, deviationUnit, unit, to } in summaryWithRouteLocations"
:data-test-id="`insights-summary-tab-${id}`" :key="id"
> :data-test-id="`insights-summary-tab-${id}`"
<router-link :to="to" :exact-active-class="$style.activeTab" @click="trackTabClick(id)"> >
<strong> <router-link :to="to" :exact-active-class="$style.activeTab" @click="trackTabClick(id)">
<N8nTooltip placement="bottom" :disabled="id !== 'timeSaved'"> <strong>
<template #content> <N8nTooltip placement="bottom" :disabled="id !== 'timeSaved'">
{{ i18n.baseText('insights.banner.title.timeSaved.tooltip') }}
</template>
{{ summaryTitles[id] }}
</N8nTooltip>
</strong>
<small :class="$style.days">{{
i18n.baseText('insights.lastNDays', { interpolate: { count: lastNDays } })
}}</small>
<span v-if="value === 0 && id === 'timeSaved'" :class="$style.empty">
<em>--</em>
<small>
<N8nTooltip placement="bottom">
<template #content> <template #content>
<i18n-t keypath="insights.banner.timeSaved.tooltip"> {{ i18n.baseText('insights.banner.title.timeSaved.tooltip') }}
<template #link>
<a href="#">{{
i18n.baseText('insights.banner.timeSaved.tooltip.link.text')
}}</a>
</template>
</i18n-t>
</template> </template>
<N8nIcon :class="$style.icon" icon="info-circle" /> {{ summaryTitles[id] }}
</N8nTooltip> </N8nTooltip>
</small> </strong>
</span> <small :class="$style.days">{{
<span v-else> i18n.baseText('insights.lastNDays', { interpolate: { count: lastNDays } })
<em }}</small>
>{{ smartDecimal(value).toLocaleString('en-US') }} <i>{{ unit }}</i></em <span v-if="value === 0 && id === 'timeSaved'" :class="$style.empty">
> <em>--</em>
<small v-if="deviation !== null" :class="getImpactStyle(id, deviation)"> <small>
<N8nIcon <N8nTooltip placement="bottom">
:class="[$style.icon, getImpactStyle(id, deviation)]" <template #content>
:icon="deviation === 0 ? 'caret-right' : deviation > 0 ? 'caret-up' : 'caret-down'" <i18n-t keypath="insights.banner.timeSaved.tooltip">
/> <template #link>
<N8nTooltip placement="bottom" :disabled="id !== 'failureRate'"> <a href="#">{{
<template #content> i18n.baseText('insights.banner.timeSaved.tooltip.link.text')
{{ i18n.baseText('insights.banner.failureRate.deviation.tooltip') }} }}</a>
</template> </template>
{{ smartDecimal(Math.abs(deviation)).toLocaleString('en-US') }}{{ deviationUnit }} </i18n-t>
</N8nTooltip> </template>
</small> <N8nIcon :class="$style.icon" icon="info-circle" />
</span> </N8nTooltip>
</router-link> </small>
</li> </span>
<span v-else>
<em
>{{ smartDecimal(value).toLocaleString('en-US') }} <i>{{ unit }}</i></em
>
<small v-if="deviation !== null" :class="getImpactStyle(id, deviation)">
<N8nIcon
:class="[$style.icon, getImpactStyle(id, deviation)]"
:icon="
deviation === 0 ? 'caret-right' : deviation > 0 ? 'caret-up' : 'caret-down'
"
/>
<N8nTooltip placement="bottom" :disabled="id !== 'failureRate'">
<template #content>
{{ i18n.baseText('insights.banner.failureRate.deviation.tooltip') }}
</template>
{{ smartDecimal(Math.abs(deviation)).toLocaleString('en-US') }}{{ deviationUnit }}
</N8nTooltip>
</small>
</span>
</router-link>
</li>
</template>
</ul> </ul>
</div> </div>
</template> </template>
@@ -222,7 +226,6 @@ const trackTabClick = (insightType: keyof InsightsSummary) => {
gap: var(--spacing-5xs); gap: var(--spacing-5xs);
i { i {
color: var(--color-text-light);
font-size: 22px; font-size: 22px;
font-style: normal; font-style: normal;
} }
@@ -268,13 +271,13 @@ const trackTabClick = (insightType: keyof InsightsSummary) => {
.loading { .loading {
display: flex; display: flex;
min-height: 91px;
align-self: stretch; align-self: stretch;
align-items: stretch; align-items: stretch;
> div { > div {
margin: 0; margin: 0;
height: auto; height: auto;
border-radius: inherit;
} }
} }
</style> </style>

View File

@@ -1,14 +1,14 @@
<script lang="ts" setup> <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 { 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 { computed } from 'vue';
import { Bar } from 'vue-chartjs'; import { Bar } from 'vue-chartjs';
import type { ChartData } from 'chart.js';
import { useCssVar } from '@vueuse/core';
import dateformat from 'dateformat';
import type { InsightsByTime, InsightsSummaryType } from '@n8n/api-types';
import { generateBarChartOptions } from '@/features/insights/chartjs.utils';
import { useI18n } from '@/composables/useI18n';
import { smartDecimal } from '@n8n/utils/number/smartDecimal';
import { DATE_FORMAT_MASK } from '@/features/insights/insights.constants';
const props = defineProps<{ const props = defineProps<{
data: InsightsByTime[]; data: InsightsByTime[];
@@ -46,7 +46,7 @@ const chartData = computed<ChartData<'bar'>>(() => {
labels, labels,
datasets: [ datasets: [
{ {
label: i18n.baseText('insights.banner.title.failed'), label: i18n.baseText('insights.chart.failed'),
data, data,
backgroundColor: colorPrimary.value, backgroundColor: colorPrimary.value,
}, },

View File

@@ -1,13 +1,13 @@
<script lang="ts" setup> <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 { useCssVar } from '@vueuse/core';
import type { ChartData } from 'chart.js';
import dateformat from 'dateformat';
import { computed } from 'vue'; import { computed } from 'vue';
import { Bar } from 'vue-chartjs'; import { Bar } from 'vue-chartjs';
import type { ChartData } from 'chart.js';
import { useCssVar } from '@vueuse/core';
import dateformat from 'dateformat';
import type { InsightsByTime, InsightsSummaryType } from '@n8n/api-types';
import { generateBarChartOptions } from '@/features/insights/chartjs.utils';
import { useI18n } from '@/composables/useI18n';
import { DATE_FORMAT_MASK } from '@/features/insights/insights.constants';
const props = defineProps<{ const props = defineProps<{
data: InsightsByTime[]; data: InsightsByTime[];
@@ -21,8 +21,7 @@ const chartOptions = computed(() =>
generateBarChartOptions({ generateBarChartOptions({
plugins: { plugins: {
tooltip: { tooltip: {
itemSort: (a) => itemSort: (a) => (a.dataset.label === i18n.baseText('insights.chart.succeeded') ? -1 : 1),
a.dataset.label === i18n.baseText('insights.banner.title.succeeded') ? -1 : 1,
}, },
}, },
}), }),
@@ -43,12 +42,12 @@ const chartData = computed<ChartData<'bar'>>(() => {
labels, labels,
datasets: [ datasets: [
{ {
label: i18n.baseText('insights.banner.title.failed'), label: i18n.baseText('insights.chart.failed'),
data: failedData, data: failedData,
backgroundColor: colorPrimary.value, backgroundColor: colorPrimary.value,
}, },
{ {
label: i18n.baseText('insights.banner.title.succeeded'), label: i18n.baseText('insights.chart.succeeded'),
data: succeededData, data: succeededData,
backgroundColor: '#3E999F', backgroundColor: '#3E999F',
}, },

View File

@@ -3083,12 +3083,13 @@
"insights.banner.timeSaved.tooltip.link.text": "add time estimates", "insights.banner.timeSaved.tooltip.link.text": "add time estimates",
"insights.banner.title.total": "Prod. executions", "insights.banner.title.total": "Prod. executions",
"insights.banner.title.failed": "Failed prod. executions", "insights.banner.title.failed": "Failed prod. executions",
"insights.banner.title.succeeded": "Successful prod. executions",
"insights.banner.title.failureRate": "Failure rate", "insights.banner.title.failureRate": "Failure rate",
"insights.banner.title.timeSaved": "Time saved", "insights.banner.title.timeSaved": "Time saved",
"insights.banner.title.timeSavedDailyAverage": "Time saved daily avg.", "insights.banner.title.timeSavedDailyAverage": "Time saved daily avg.",
"insights.banner.title.averageRunTime": "Run time (avg.)", "insights.banner.title.averageRunTime": "Run time (avg.)",
"insights.dashboard.title": "Insights", "insights.dashboard.title": "Insights",
"insights.banner.title.timeSaved.tooltip": "Total time saved calculated from your estimated time savings per execution across all workflows", "insights.banner.title.timeSaved.tooltip": "Total time saved calculated from your estimated time savings per execution across all workflows",
"insights.banner.failureRate.deviation.tooltip": "Percentage point change from previous period" "insights.banner.failureRate.deviation.tooltip": "Percentage point change from previous period",
"insights.chart.failed": "Failed",
"insights.chart.succeeded": "Successful"
} }