fix(editor): Feature/table reskin (no-changelog) (#13817)

This commit is contained in:
Raúl Gómez Morales
2025-03-12 11:04:27 +01:00
committed by GitHub
parent 48eef63bf3
commit 09ebc3adc7
15 changed files with 705 additions and 695 deletions

View File

@@ -0,0 +1,86 @@
<template>
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<g>
<path d="M7 0L7 3" stroke="currentColor" stroke-width="2" class="line one" />
<path
d="M11.9497 2.05031L9.82837 4.17163"
stroke="currentColor"
stroke-width="2"
class="line two"
/>
<path d="M14 7L11 7" stroke="currentColor" stroke-width="2" class="line three" />
<path
d="M11.9497 11.9497L9.82839 9.82839"
stroke="currentColor"
stroke-width="2"
class="line four"
/>
<path d="M7 14L7 11" stroke="currentColor" stroke-width="2" class="line five" />
<path d="M0 7L3 7" stroke="currentColor" stroke-width="2" class="line seven" />
<path
d="M2.05031 2.05031L4.17163 4.17163"
stroke="currentColor"
stroke-width="2"
class="line eight"
/>
<path
d="M2.05029 11.9497L4.17161 9.82839"
stroke="currentColor"
stroke-width="2"
class="line six"
/>
</g>
</svg>
</template>
<style scoped>
@keyframes fade {
0% {
opacity: 100;
}
20% {
opacity: 0;
}
40% {
opacity: 100;
}
}
.line {
animation: fade 1.6s infinite;
}
.one {
animation-delay: 0s;
}
.two {
animation-delay: 0.2s;
}
.three {
animation-delay: 0.4s;
}
.four {
animation-delay: 0.6s;
}
.five {
animation-delay: 0.8s;
}
.six {
animation-delay: 1s;
}
.seven {
animation-delay: 1.2s;
}
.eight {
animation-delay: 1.4s;
}
</style>

View File

@@ -1,8 +1,8 @@
<script lang="ts" setup>
import type { EnvironmentVariable, Rule, RuleGroup } from '@/Interface';
import { useI18n } from '@/composables/useI18n';
import { computed, ref, reactive, toRaw } from 'vue';
import { N8nFormInput, N8nButton } from '@n8n/design-system';
import { N8nButton, N8nFormInput } from '@n8n/design-system';
import { computed, reactive, ref, toRaw } from 'vue';
import VariablesUsageBadge from './VariablesUsageBadge.vue';
const props = defineProps<{
@@ -113,11 +113,6 @@ const handleSubmit = () => {
</template>
<style lang="scss" scoped>
.key-cell,
.value-cell {
vertical-align: top;
}
.value-cell {
width: 100%;
max-width: 50%;

View File

@@ -1,22 +1,25 @@
<script lang="ts" setup>
import { watch, computed, ref, onMounted } from 'vue';
import ConcurrentExecutionsHeader from '@/components/executions/ConcurrentExecutionsHeader.vue';
import ExecutionsFilter from '@/components/executions/ExecutionsFilter.vue';
import GlobalExecutionsListItem from '@/components/executions/global/GlobalExecutionsListItem.vue';
import { EnterpriseEditionFeature, MODAL_CONFIRM } from '@/constants';
import { useToast } from '@/composables/useToast';
import { useMessage } from '@/composables/useMessage';
import ProjectHeader from '@/components/Projects/ProjectHeader.vue';
import { useI18n } from '@/composables/useI18n';
import { useMessage } from '@/composables/useMessage';
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
import { useTelemetry } from '@/composables/useTelemetry';
import { useToast } from '@/composables/useToast';
import { EnterpriseEditionFeature, MODAL_CONFIRM } from '@/constants';
import type { ExecutionFilterType, ExecutionSummaryWithScopes, IWorkflowDb } from '@/Interface';
import type { ExecutionSummary } from 'n8n-workflow';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useExecutionsStore } from '@/stores/executions.store';
import type { PermissionsRecord } from '@/permissions';
import { getResourcePermissions } from '@/permissions';
import { useExecutionsStore } from '@/stores/executions.store';
import { useSettingsStore } from '@/stores/settings.store';
import ProjectHeader from '@/components/Projects/ProjectHeader.vue';
import ConcurrentExecutionsHeader from '@/components/executions/ConcurrentExecutionsHeader.vue';
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { N8nButton, N8nCheckbox, N8nTableBase } from '@n8n/design-system';
import { useIntersectionObserver } from '@vueuse/core';
import { ElSkeletonItem } from 'element-plus';
import type { ExecutionSummary } from 'n8n-workflow';
import { computed, ref, useTemplateRef, watch, type ComponentPublicInstance } from 'vue';
const props = withDefaults(
defineProps<{
@@ -43,7 +46,6 @@ const executionsStore = useExecutionsStore();
const settingsStore = useSettingsStore();
const pageRedirectionHelper = usePageRedirectionHelper();
const isMounted = ref(false);
const allVisibleSelected = ref(false);
const allExistingSelected = ref(false);
const selectedItems = ref<Record<string, boolean>>({});
@@ -94,10 +96,6 @@ watch(
},
);
onMounted(() => {
isMounted.value = true;
});
function handleCheckAllExistingChange() {
allExistingSelected.value = !allExistingSelected.value;
allVisibleSelected.value = !allExistingSelected.value;
@@ -203,19 +201,21 @@ function getWorkflowName(workflowId: string): string | undefined {
return workflows.value.find((data: IWorkflowDb) => data.id === workflowId)?.name;
}
const loadMoreRef = useTemplateRef<ComponentPublicInstance>('loadMoreButton');
useIntersectionObserver(loadMoreRef, ([entry]) => {
if (!entry?.isIntersecting) return;
void loadMore();
});
async function loadMore() {
if (executionsStore.filters.status === 'running') {
return;
}
let lastId: string | undefined;
if (props.executions.length !== 0) {
const lastItem = props.executions.slice(-1)[0];
lastId = lastItem.id;
}
const lastItem = props.executions.at(-1);
try {
await executionsStore.fetchExecutions(executionsStore.executionsFilters, lastId);
await executionsStore.fetchExecutions(executionsStore.executionsFilters, lastItem?.id);
} catch (error) {
toast.showError(error, i18n.baseText('executionsList.showError.loadMore.title'));
}
@@ -335,121 +335,131 @@ const goToUpgrade = () => {
<template>
<div :class="$style.execListWrapper">
<ProjectHeader />
<div :class="$style.execList">
<div :class="$style.execListHeader">
<div :class="$style.execListHeaderControls">
<ConcurrentExecutionsHeader
v-if="settingsStore.isConcurrencyEnabled"
class="mr-xl"
:running-executions-count="runningExecutionsCount"
:concurrency-cap="settingsStore.concurrency"
:is-cloud-deployment="settingsStore.isCloudDeployment"
@go-to-upgrade="goToUpgrade"
/>
<N8nLoading v-if="!isMounted" :class="$style.filterLoader" variant="custom" />
<ElCheckbox
v-else
v-model="executionsStore.autoRefresh"
class="mr-xl"
data-test-id="execution-auto-refresh-checkbox"
@update:model-value="onAutoRefreshToggle($event)"
>
{{ i18n.baseText('executionsList.autoRefresh') }}
</ElCheckbox>
<ExecutionsFilter
v-show="isMounted"
:workflows="workflows"
class="execFilter"
@filter-changed="onFilterChanged"
/>
</div>
</div>
<ElCheckbox
v-if="allVisibleSelected && total > 0"
:class="$style.selectAll"
:label="
i18n.baseText('executionsList.selectAll', {
adjustToNumber: total,
interpolate: { executionNum: `${total}` },
})
"
:model-value="allExistingSelected"
data-test-id="select-all-executions-checkbox"
@update:model-value="handleCheckAllExistingChange"
<div :class="$style.execListHeaderControls">
<ExecutionsFilter
:workflows="workflows"
class="execFilter"
@filter-changed="onFilterChanged"
/>
<div v-if="!isMounted">
<N8nLoading :class="$style.tableLoader" variant="custom" />
<N8nLoading :class="$style.tableLoader" variant="custom" />
<N8nLoading :class="$style.tableLoader" variant="custom" />
</div>
<table v-else :class="$style.execTable">
<thead>
<tr>
<th>
<el-checkbox
:model-value="allVisibleSelected"
:disabled="total < 1"
label=""
data-test-id="select-visible-executions-checkbox"
@update:model-value="handleCheckAllVisibleChange"
/>
</th>
<th>{{ i18n.baseText('executionsList.name') }}</th>
<th>{{ i18n.baseText('executionsList.startedAt') }}</th>
<th>{{ i18n.baseText('executionsList.status') }}</th>
<th>{{ i18n.baseText('executionsList.id') }}</th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<TransitionGroup tag="tbody" name="executions-list">
<GlobalExecutionsListItem
v-for="execution in executions"
:key="execution.id"
:execution="execution"
:workflow-name="getExecutionWorkflowName(execution)"
:workflow-permissions="getExecutionWorkflowPermissions(execution)"
:selected="selectedItems[execution.id] || allExistingSelected"
:concurrency-cap="settingsStore.concurrency"
:is-cloud-deployment="settingsStore.isCloudDeployment"
data-test-id="global-execution-list-item"
@stop="stopExecution"
@delete="deleteExecution"
@select="toggleSelectExecution"
@retry-saved="retrySavedExecution"
@retry-original="retryOriginalExecution"
@go-to-upgrade="goToUpgrade"
/>
</TransitionGroup>
</table>
<div
v-if="!executions.length && isMounted && !executionsStore.loading"
:class="$style.loadedAll"
data-test-id="execution-list-empty"
>
{{ i18n.baseText('executionsList.empty') }}
</div>
<div v-else-if="total > executions.length || estimated" :class="$style.loadMore">
<N8nButton
icon="sync"
:title="i18n.baseText('executionsList.loadMore')"
:label="i18n.baseText('executionsList.loadMore')"
:loading="executionsStore.loading"
data-test-id="load-more-button"
@click="loadMore()"
<div style="margin-left: auto">
<ConcurrentExecutionsHeader
v-if="settingsStore.isConcurrencyEnabled"
:running-executions-count="runningExecutionsCount"
:concurrency-cap="settingsStore.concurrency"
:is-cloud-deployment="settingsStore.isCloudDeployment"
@go-to-upgrade="goToUpgrade"
/>
<ElCheckbox
v-else
v-model="executionsStore.autoRefresh"
data-test-id="execution-auto-refresh-checkbox"
@update:model-value="onAutoRefreshToggle($event)"
>
{{ i18n.baseText('executionsList.autoRefresh') }}
</ElCheckbox>
</div>
<div
v-else-if="isMounted && !executionsStore.loading"
:class="$style.loadedAll"
data-test-id="execution-all-loaded"
>
{{ i18n.baseText('executionsList.loadedAll') }}
</div>
<div :class="$style.execList">
<div :class="$style.execTable">
<N8nTableBase>
<thead>
<tr v-if="allVisibleSelected && total > 0">
<th style="width: 50px">
<N8nCheckbox
:model-value="allExistingSelected"
data-test-id="select-all-executions-checkbox"
@update:model-value="handleCheckAllExistingChange"
/>
</th>
<th colspan="8">
{{
i18n.baseText('executionsList.selectAll', {
adjustToNumber: total,
interpolate: { executionNum: `${total}` },
})
}}
</th>
</tr>
<tr>
<th style="width: 50px">
<N8nCheckbox
:model-value="allVisibleSelected"
:disabled="total < 1"
data-test-id="select-visible-executions-checkbox"
@update:model-value="handleCheckAllVisibleChange"
/>
</th>
<th>
{{ i18n.baseText('generic.workflow') }}
</th>
<th>{{ i18n.baseText('executionsList.status') }}</th>
<th>
{{ i18n.baseText('executionsList.startedAt') }}
</th>
<th>
{{ i18n.baseText('executionsList.runTime') }}
</th>
<th>{{ i18n.baseText('executionsList.id') }}</th>
<th>
{{ i18n.baseText('executionsList.trigger') }}
</th>
<th style="width: 69px"></th>
<th style="width: 50px"></th>
</tr>
</thead>
<tbody>
<GlobalExecutionsListItem
v-for="execution in executions"
:key="execution.id"
:execution="execution"
:workflow-name="getExecutionWorkflowName(execution)"
:workflow-permissions="getExecutionWorkflowPermissions(execution)"
:selected="selectedItems[execution.id] || allExistingSelected"
:concurrency-cap="settingsStore.concurrency"
:is-cloud-deployment="settingsStore.isCloudDeployment"
data-test-id="global-execution-list-item"
@stop="stopExecution"
@delete="deleteExecution"
@select="toggleSelectExecution"
@retry-saved="retrySavedExecution"
@retry-original="retryOriginalExecution"
@go-to-upgrade="goToUpgrade"
/>
<template v-if="executionsStore.loading && !executions.length">
<tr v-for="item in executionsStore.itemsPerPage" :key="item">
<td v-for="col in 9" :key="col">
<ElSkeletonItem />
</td>
</tr>
</template>
<tr>
<td colspan="9" style="text-align: center">
<template v-if="!executions.length">
<span data-test-id="execution-list-empty">
{{ i18n.baseText('executionsList.empty') }}
</span>
</template>
<template v-else-if="total > executions.length || estimated">
<N8nButton
ref="loadMoreButton"
icon="sync"
:title="i18n.baseText('executionsList.loadMore')"
:label="i18n.baseText('executionsList.loadMore')"
:loading="executionsStore.loading"
data-test-id="load-more-button"
@click="loadMore()"
/>
</template>
<template v-else>
{{ i18n.baseText('executionsList.loadedAll') }}
</template>
</td>
</tr>
</tbody>
</N8nTableBase>
</div>
</div>
<div
@@ -483,36 +493,25 @@ const goToUpgrade = () => {
<style module lang="scss">
.execListWrapper {
display: grid;
grid-template-rows: auto auto 1fr 0;
position: relative;
height: 100%;
padding: var(--spacing-l) var(--spacing-2xl);
display: flex;
flex-direction: column;
overflow: hidden;
width: 100%;
padding: var(--spacing-l) var(--spacing-2xl) 0;
max-width: var(--content-container-width);
@include mixins.breakpoint('xs-only') {
padding: var(--spacing-xs) var(--spacing-xs) 0;
}
}
.execList {
position: relative;
height: 100%;
overflow: auto;
}
.execListHeader {
display: flex;
align-items: center;
justify-content: flex-start;
margin-bottom: var(--spacing-s);
flex-shrink: 1; /* Allows shrinking when needed */
max-height: 100%; /* Prevents overflowing the parent */
overflow: auto; /* Scroll only when needed */
}
.execListHeaderControls {
display: flex;
align-items: center;
justify-content: flex-end;
justify-content: flex-start;
margin-bottom: var(--spacing-s);
}
.selectionOptions {
@@ -535,87 +534,8 @@ const goToUpgrade = () => {
}
.execTable {
/*
Table height needs to be set to 0 in order to use height 100% for elements in table cells
*/
height: 0;
width: 100%;
text-align: left;
font-size: var(--font-size-s);
thead th {
position: sticky;
top: calc(var(--spacing-3xl) * -1);
z-index: 2;
padding: var(--spacing-s) var(--spacing-s) var(--spacing-s) 0;
background: var(--color-table-header-background);
&:first-child {
padding-left: var(--spacing-s);
}
}
th,
td {
height: 100%;
padding: var(--spacing-s) var(--spacing-s) var(--spacing-s) 0;
&:not(:first-child, :nth-last-child(-n + 3)) {
width: 100%;
}
&:nth-last-child(-n + 2) {
padding-left: 0;
}
@media (min-width: $breakpoint-sm) {
&:not(:nth-child(2)) {
&,
div,
span {
white-space: nowrap;
}
}
}
}
}
.loadMore {
margin: var(--spacing-m) 0;
width: 100%;
text-align: center;
}
.loadedAll {
text-align: center;
font-size: var(--font-size-s);
color: var(--color-text-light);
margin: var(--spacing-l) 0;
}
.actions.deleteOnly {
padding: 0;
}
.retryAction + .deleteAction {
border-top: 1px solid var(--color-foreground-light);
}
.selectAll {
display: inline-block;
margin: 0 0 var(--spacing-s) var(--spacing-s);
color: var(--execution-select-all-text);
}
.filterLoader {
width: 220px;
height: 32px;
}
.tableLoader {
width: 100%;
height: 48px;
margin-bottom: var(--spacing-2xs);
height: 100%;
flex: 0 1 auto;
}
</style>

View File

@@ -98,21 +98,6 @@ describe('GlobalExecutionsListItem', () => {
expect(emitted().delete).toBeTruthy();
});
it('should open a new window on execution click', async () => {
global.window.open = vi.fn();
const { getByText } = renderComponent({
props: {
execution: { status: 'success', id: 123, workflowName: 'TestWorkflow' },
workflowPermissions: {},
},
});
await fireEvent.click(getByText('TestWorkflow'));
expect(window.open).toHaveBeenCalledWith('mockedRoute', '_blank');
expect(globalExecutionsListItemQueuedTooltipRenderSpy).not.toHaveBeenCalled();
});
it('should show formatted start date', () => {
const testDate = '2022-01-01T12:00:00Z';
const { getByText } = renderComponent({
@@ -123,7 +108,7 @@ describe('GlobalExecutionsListItem', () => {
});
expect(
getByText(`1 Jan, 2022 at ${DateTime.fromJSDate(new Date(testDate)).toFormat('HH')}:00:00`),
getByText(`1 Jan, 2022, ${DateTime.fromJSDate(new Date(testDate)).toFormat('HH')}:00:00`),
).toBeInTheDocument();
});

View File

@@ -1,14 +1,23 @@
<script lang="ts" setup>
import { ref, computed, useCssModule } from 'vue';
import type { ExecutionSummary } from 'n8n-workflow';
import { WAIT_INDEFINITELY } from 'n8n-workflow';
import { useI18n } from '@/composables/useI18n';
import { convertToDisplayDate } from '@/utils/formatters/dateFormatter';
import { i18n as locale } from '@/plugins/i18n';
import AnimatedSpinner from '@/components/AnimatedSpinner.vue';
import ExecutionsTime from '@/components/executions/ExecutionsTime.vue';
import { useExecutionHelpers } from '@/composables/useExecutionHelpers';
import { useI18n } from '@/composables/useI18n';
import { VIEWS } from '@/constants';
import type { PermissionsRecord } from '@/permissions';
import GlobalExecutionsListItemQueuedTooltip from '@/components/executions/global/GlobalExecutionsListItemQueuedTooltip.vue';
import { convertToDisplayDate } from '@/utils/formatters/dateFormatter';
import {
N8nButton,
N8nCheckbox,
N8nIcon,
N8nIconButton,
N8nText,
N8nTooltip,
} from '@n8n/design-system';
import type { IconColor } from '@n8n/design-system/types/icon';
import type { ExecutionStatus, ExecutionSummary } from 'n8n-workflow';
import { WAIT_INDEFINITELY } from 'n8n-workflow';
import { computed, ref, useCssModule } from 'vue';
type Command = 'retrySaved' | 'retryOriginal' | 'delete';
@@ -37,18 +46,12 @@ const props = withDefaults(
);
const style = useCssModule();
const i18n = useI18n();
const locale = useI18n();
const executionHelpers = useExecutionHelpers();
const isStopping = ref(false);
const isRunning = computed(() => {
return props.execution.status === 'running';
});
const isQueued = computed(() => {
return props.execution.status === 'new';
});
const isRunning = computed(() => props.execution.status === 'running');
const isWaitTillIndefinite = computed(() => {
if (!props.execution.waitTill) {
@@ -60,17 +63,63 @@ const isWaitTillIndefinite = computed(() => {
const isRetriable = computed(() => executionHelpers.isExecutionRetriable(props.execution));
const EXECUTION_STATUS = {
CRASHED: 'crashed',
ERROR: 'error',
WAITING: 'waiting',
SUCCESS: 'success',
NEW: 'new',
RUNNING: 'running',
UNKNOWN: 'unknown',
CANCELED: 'canceled',
} as const;
const executionIconStatusDictionary: Record<ExecutionStatus, { icon: string; color: IconColor }> = {
[EXECUTION_STATUS.CRASHED]: {
icon: 'status-error',
color: 'danger',
},
[EXECUTION_STATUS.ERROR]: {
icon: 'status-error',
color: 'danger',
},
[EXECUTION_STATUS.WAITING]: {
icon: 'status-waiting',
color: 'secondary',
},
[EXECUTION_STATUS.SUCCESS]: {
icon: 'status-completed',
color: 'success',
},
[EXECUTION_STATUS.NEW]: {
icon: 'status-new',
color: 'foreground-xdark',
},
[EXECUTION_STATUS.RUNNING]: {
icon: 'spinner',
color: 'secondary',
},
[EXECUTION_STATUS.UNKNOWN]: {
icon: 'status-unknown',
color: 'foreground-xdark',
},
[EXECUTION_STATUS.CANCELED]: {
icon: 'status-canceled',
color: 'foreground-xdark',
},
};
const errorStatuses: ExecutionStatus[] = [EXECUTION_STATUS.ERROR, EXECUTION_STATUS.CRASHED];
const classes = computed(() => {
return {
[style.executionListItem]: true,
[style[props.execution.status]]: true,
[style.dangerBg]: errorStatuses.includes(props.execution.status),
};
});
const formattedStartedAtDate = computed(() => {
return props.execution.startedAt
? formatDate(props.execution.startedAt)
: i18n.baseText('executionsList.startingSoon');
: locale.baseText('executionsList.startingSoon');
});
const formattedWaitTillDate = computed(() => {
@@ -79,7 +128,7 @@ const formattedWaitTillDate = computed(() => {
const formattedStoppedAtDate = computed(() => {
return props.execution.stoppedAt
? i18n.displayTimer(
? locale.displayTimer(
new Date(props.execution.stoppedAt).getTime() -
new Date(props.execution.startedAt).getTime(),
true,
@@ -87,48 +136,18 @@ const formattedStoppedAtDate = computed(() => {
: '';
});
const statusText = computed(() => {
switch (props.execution.status) {
case 'waiting':
return i18n.baseText('executionsList.waiting');
case 'canceled':
return i18n.baseText('executionsList.canceled');
case 'crashed':
return i18n.baseText('executionsList.error');
case 'new':
return i18n.baseText('executionsList.new');
case 'running':
return i18n.baseText('executionsList.running');
case 'success':
return i18n.baseText('executionsList.succeeded');
case 'error':
return i18n.baseText('executionsList.error');
default:
return i18n.baseText('executionsList.unknown');
function getStatusLabel(status: ExecutionStatus) {
if (status === EXECUTION_STATUS.CRASHED) {
return locale.baseText('executionsList.error');
}
});
return locale.baseText(`executionsList.${status}`);
}
const statusTextTranslationPath = computed(() => {
switch (props.execution.status) {
case 'waiting':
return 'executionsList.statusWaiting';
case 'canceled':
return 'executionsList.statusCanceled';
case 'crashed':
case 'error':
case 'success':
if (!props.execution.stoppedAt) {
return 'executionsList.statusTextWithoutTime';
} else {
return 'executionsList.statusText';
}
case 'new':
return 'executionsList.statusTextWithoutTime';
case 'running':
return 'executionsList.statusRunning';
default:
return 'executionsList.statusUnknown';
}
const statusRender = computed(() => {
return {
...executionIconStatusDictionary[props.execution.status],
label: getStatusLabel(props.execution.status),
};
});
function formatDate(fullDate: Date | string | number) {
@@ -136,10 +155,6 @@ function formatDate(fullDate: Date | string | number) {
return locale.baseText('executionsList.started', { interpolate: { time, date } });
}
function displayExecution() {
executionHelpers.openExecutionInNewTab(props.execution.id, props.execution.workflowId);
}
function onStopExecution() {
isStopping.value = true;
emit('stop', props.execution);
@@ -157,108 +172,96 @@ async function handleActionItemClick(commandData: Command) {
<template>
<tr :class="classes">
<td>
<ElCheckbox
v-if="!!execution.stoppedAt && execution.id"
<N8nCheckbox
:model-value="selected"
label=""
data-test-id="select-execution-checkbox"
:disabled="!Boolean(execution.id && execution.stoppedAt)"
@update:model-value="onSelect"
/>
</td>
<td>
<span :class="$style.link" @click.stop="displayExecution">
{{ execution.workflowName || workflowName }}
</span>
<RouterLink
:to="{
name: VIEWS.EXECUTION_PREVIEW,
params: { name: execution.workflowId, executionId: execution.id },
}"
target="_blank"
>
<N8nText color="text-dark">
{{ execution.workflowName || workflowName }}
</N8nText>
</RouterLink>
</td>
<td data-test-id="execution-status">
<GlobalExecutionsListItemQueuedTooltip
v-if="isWaitTillIndefinite || execution.status === EXECUTION_STATUS.NEW"
:status="props.execution.status"
:concurrency-cap="props.concurrencyCap"
:is-cloud-deployment="props.isCloudDeployment"
@go-to-upgrade="emit('goToUpgrade')"
>
<div>
<N8nIcon :icon="statusRender.icon" :color="statusRender.color" class="mr-2xs" />
{{ statusRender.label }}
</div>
</GlobalExecutionsListItemQueuedTooltip>
<N8nTooltip
v-else
:disabled="execution.status !== EXECUTION_STATUS.WAITING"
:content="
locale.baseText('executionsList.statusWaiting', {
interpolate: { status: execution.status, time: formattedWaitTillDate },
})
"
>
<div>
<N8nText
v-if="execution.status === EXECUTION_STATUS.RUNNING"
color="secondary"
class="mr-2xs"
>
<AnimatedSpinner />
</N8nText>
<N8nIcon v-else :icon="statusRender.icon" :color="statusRender.color" class="mr-2xs" />
{{ statusRender.label }}
</div>
</N8nTooltip>
</td>
<td>
<span>{{ formattedStartedAtDate }}</span>
{{ formattedStartedAtDate }}
</td>
<td>
<div :class="$style.statusColumn">
<span v-if="isRunning" :class="$style.spinner">
<FontAwesomeIcon icon="spinner" spin />
</span>
<i18n-t
v-if="!isWaitTillIndefinite && !isQueued"
data-test-id="execution-status"
tag="span"
:keypath="statusTextTranslationPath"
>
<template #status>
<span :class="$style.status">{{ statusText }}</span>
</template>
<template #time>
<span v-if="execution.waitTill">{{ formattedWaitTillDate }}</span>
<span v-else-if="!!execution.stoppedAt">
{{ formattedStoppedAtDate }}
</span>
<ExecutionsTime
v-else-if="execution.status !== 'new'"
:start-time="execution.startedAt"
/>
</template>
</i18n-t>
<GlobalExecutionsListItemQueuedTooltip
v-else
:status="props.execution.status"
:concurrency-cap="props.concurrencyCap"
:is-cloud-deployment="props.isCloudDeployment"
@go-to-upgrade="emit('goToUpgrade')"
>
<span :class="$style.status">{{ statusText }}</span>
</GlobalExecutionsListItemQueuedTooltip>
</div>
<template v-if="formattedStoppedAtDate">
{{ formattedStoppedAtDate }}
</template>
<ExecutionsTime v-else :start-time="execution.startedAt" />
</td>
<td>
<span v-if="execution.id">#{{ execution.id }}</span>
<span v-if="execution.id">{{ execution.id }}</span>
<span v-if="execution.retryOf">
<br />
<small> ({{ i18n.baseText('executionsList.retryOf') }} #{{ execution.retryOf }}) </small>
<small> ({{ locale.baseText('executionsList.retryOf') }} {{ execution.retryOf }}) </small>
</span>
<span v-else-if="execution.retrySuccessId">
<br />
<small>
({{ i18n.baseText('executionsList.successRetry') }} #{{ execution.retrySuccessId }})
({{ locale.baseText('executionsList.successRetry') }} {{ execution.retrySuccessId }})
</small>
</span>
</td>
<td>
<N8nTooltip v-if="execution.mode === 'manual'" placement="top">
<template #content>
<span>{{ i18n.baseText('executionsList.test') }}</span>
</template>
<FontAwesomeIcon icon="flask" />
</N8nTooltip>
<N8nTooltip v-if="execution.mode === 'evaluation'" placement="top">
<template #content>
<span>{{ i18n.baseText('executionsList.evaluation') }}</span>
</template>
<FontAwesomeIcon icon="tasks" />
</N8nTooltip>
<span :class="$style.capitalize">{{ execution.mode }}</span>
</td>
<td>
<div :class="$style.buttonCell">
<N8nButton
v-if="!!execution.stoppedAt && execution.id"
size="small"
outline
:label="i18n.baseText('executionsList.view')"
@click.stop="displayExecution"
/>
</div>
</td>
<td>
<div :class="$style.buttonCell">
<N8nButton
v-if="!execution.stoppedAt || execution.waitTill"
data-test-id="stop-execution-button"
size="small"
outline
:label="i18n.baseText('executionsList.stop')"
:loading="isStopping"
@click.stop="onStopExecution"
/>
</div>
<N8nButton
v-if="!execution.stoppedAt || execution.waitTill"
data-test-id="stop-execution-button"
:loading="isStopping"
:disabled="isStopping"
@click.stop="onStopExecution"
>
{{ locale.baseText('executionsList.stop') }}
</N8nButton>
</td>
<td>
<ElDropdown v-if="!isRunning" trigger="click" @command="handleActionItemClick">
@@ -277,7 +280,7 @@ async function handleActionItemClick(commandData: Command) {
command="retrySaved"
:disabled="!workflowPermissions.execute"
>
{{ i18n.baseText('executionsList.retryWithCurrentlySavedWorkflow') }}
{{ locale.baseText('executionsList.retryWithCurrentlySavedWorkflow') }}
</ElDropdownItem>
<ElDropdownItem
v-if="isRetriable"
@@ -286,7 +289,7 @@ async function handleActionItemClick(commandData: Command) {
command="retryOriginal"
:disabled="!workflowPermissions.execute"
>
{{ i18n.baseText('executionsList.retryWithOriginalWorkflow') }}
{{ locale.baseText('executionsList.retryWithOriginalWorkflow') }}
</ElDropdownItem>
<ElDropdownItem
data-test-id="execution-delete-dropdown-item"
@@ -294,7 +297,7 @@ async function handleActionItemClick(commandData: Command) {
command="delete"
:disabled="!workflowPermissions.update"
>
{{ i18n.baseText('generic.delete') }}
{{ locale.baseText('generic.delete') }}
</ElDropdownItem>
</ElDropdownMenu>
</template>
@@ -304,126 +307,11 @@ async function handleActionItemClick(commandData: Command) {
</template>
<style lang="scss" module>
@import '@/styles/variables';
.executionListItem {
--execution-list-item-background: var(--color-table-row-background);
--execution-list-item-highlight-background: var(--color-table-row-highlight-background);
color: var(--color-text-base);
td {
background: var(--execution-list-item-background);
}
&:nth-child(even) td {
--execution-list-item-background: var(--color-table-row-even-background);
}
&:hover td {
background: var(--color-table-row-hover-background);
}
td:first-child {
width: 30px;
padding: 0 var(--spacing-s) 0 0;
/*
This is needed instead of table cell border because they are overlapping the sticky header
*/
&::before {
content: '';
display: inline-block;
width: var(--spacing-4xs);
height: 100%;
vertical-align: middle;
margin-right: var(--spacing-xs);
}
}
&.crashed td:first-child::before,
&.error td:first-child::before {
background: var(--execution-card-border-error);
}
&.success td:first-child::before {
background: var(--execution-card-border-success);
}
&.new td:first-child::before {
background: var(--execution-card-border-waiting);
}
&.running td:first-child::before {
background: var(--execution-card-border-running);
}
&.waiting td:first-child::before {
background: var(--execution-card-border-waiting);
}
&.unknown td:first-child::before {
background: var(--execution-card-border-unknown);
}
tr.dangerBg {
background-color: rgba(215, 56, 58, 0.1);
}
.link {
color: var(--color-text-base);
text-decoration: underline;
cursor: pointer;
}
.statusColumn {
display: flex;
align-items: center;
}
.spinner {
margin-right: var(--spacing-2xs);
}
.status {
line-height: 22.6px;
text-align: center;
font-size: var(--font-size-s);
font-weight: var(--font-weight-bold);
.crashed &,
.error & {
color: var(--color-danger);
}
.waiting & {
color: var(--color-secondary);
}
.success & {
font-weight: var(--font-weight-normal);
}
.new & {
color: var(--execution-card-text-waiting);
}
.running & {
color: var(--color-warning);
}
.unknown & {
color: var(--color-background-dark);
}
}
.buttonCell {
overflow: hidden;
button {
transform: translateX(1000%);
transition: transform 0s;
&:focus-visible,
.executionListItem:hover & {
transform: translateX(0);
}
}
.capitalize {
text-transform: capitalize;
}
</style>