mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
fix(editor): Feature/table reskin (no-changelog) (#13817)
This commit is contained in:
committed by
GitHub
parent
48eef63bf3
commit
09ebc3adc7
@@ -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>
|
||||
@@ -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%;
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user