+
(),
{
selected: false,
@@ -42,6 +43,10 @@ const isRunning = computed(() => {
return props.execution.status === 'running';
});
+const isQueued = computed(() => {
+ return props.execution.status === 'new';
+});
+
const isWaitTillIndefinite = computed(() => {
if (!props.execution.waitTill) {
return false;
@@ -80,6 +85,12 @@ const formattedStoppedAtDate = computed(() => {
});
const statusTooltipText = computed(() => {
+ if (isQueued.value) {
+ return i18n.baseText('executionsList.statusTooltipText.waitingForConcurrencyCapacity', {
+ interpolate: { concurrencyCap: props.concurrencyCap },
+ });
+ }
+
if (props.execution.status === 'waiting' && isWaitTillIndefinite.value) {
return i18n.baseText('executionsList.statusTooltipText.theWorkflowIsWaitingIndefinitely');
}
@@ -178,7 +189,7 @@ async function handleActionItemClick(commandData: Command) {
{
+ const location = {};
+ return {
+ useRouter: vi.fn(),
+ useRoute: () => ({
+ location,
+ }),
+ RouterLink: {
+ template: '',
+ },
+ };
+});
+
+const renderComponent = createComponentRenderer(WorkflowExecutionsSidebar, {
+ pinia: createTestingPinia({
+ initialState: {
+ [STORES.EXECUTIONS]: {
+ executions: [],
+ },
+ [STORES.SETTINGS]: {
+ settings: merge(SETTINGS_STORE_DEFAULT_STATE.settings, {
+ enterprise: {
+ advancedExecutionFilters: true,
+ },
+ }),
+ },
+ },
+ }),
+});
+
+let settingsStore: MockedStore;
+
+describe('WorkflowExecutionsSidebar', () => {
+ beforeEach(() => {
+ settingsStore = mockedStore(useSettingsStore);
+ });
+
+ it('should not throw error when opened', async () => {
+ expect(() =>
+ renderComponent({
+ props: {
+ loading: false,
+ loadingMore: false,
+ executions: [],
+ },
+ }),
+ ).not.toThrow();
+ });
+
+ it('should render concurrent executions header if the feature is enabled', async () => {
+ settingsStore.concurrency = 5;
+ const { getByTestId } = renderComponent({
+ props: {
+ loading: false,
+ loadingMore: false,
+ executions: [],
+ },
+ });
+
+ expect(getByTestId('concurrent-executions-header')).toBeVisible();
+ });
+});
diff --git a/packages/editor-ui/src/components/executions/workflow/WorkflowExecutionsSidebar.vue b/packages/editor-ui/src/components/executions/workflow/WorkflowExecutionsSidebar.vue
index 6dc874d9b9..443a3d0f5d 100644
--- a/packages/editor-ui/src/components/executions/workflow/WorkflowExecutionsSidebar.vue
+++ b/packages/editor-ui/src/components/executions/workflow/WorkflowExecutionsSidebar.vue
@@ -13,6 +13,8 @@ import type { ExecutionFilterType, IWorkflowDb } from '@/Interface';
import { isComponentPublicInstance } from '@/utils/typeGuards';
import { getResourcePermissions } from '@/permissions';
import { useI18n } from '@/composables/useI18n';
+import { useSettingsStore } from '@/stores/settings.store';
+import ConcurrentExecutionsHeader from '@/components/executions/ConcurrentExecutionsHeader.vue';
type AutoScrollDeps = { activeExecutionSet: boolean; cardsMounted: boolean; scroll: boolean };
@@ -36,6 +38,7 @@ const router = useRouter();
const i18n = useI18n();
const executionsStore = useExecutionsStore();
+const settingsStore = useSettingsStore();
const mountedItems = ref([]);
const autoScrollDeps = ref({
@@ -49,6 +52,10 @@ const executionListRef = ref(null);
const workflowPermissions = computed(() => getResourcePermissions(props.workflow?.scopes).workflow);
+const runningExecutionsCount = computed(() => {
+ return props.executions.filter((execution) => execution.status === 'running').length;
+});
+
watch(
() => route,
(to: RouteLocationNormalizedLoaded, from: RouteLocationNormalizedLoaded) => {
@@ -174,6 +181,12 @@ function scrollToActiveCard(): void {
{{ i18n.baseText('generic.executions') }}
+
+
{
const rootStore = useRootStore();
const projectsStore = useProjectsStore();
+ const settingsStore = useSettingsStore();
const loading = ref(false);
const itemsPerPage = ref(10);
@@ -67,12 +69,29 @@ export const useExecutionsStore = defineStore('executions', () => {
);
const currentExecutionsById = ref>({});
+ const startedAtSortFn = (a: ExecutionSummary, b: ExecutionSummary) =>
+ new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime();
+
+ /**
+ * Prioritize `running` over `new` executions, then sort by start timestamp.
+ */
+ const statusThenStartedAtSortFn = (a: ExecutionSummary, b: ExecutionSummary) => {
+ if (a.status && b.status) {
+ const statusPriority: { [key: string]: number } = { running: 1, new: 2 };
+ const statusComparison = statusPriority[a.status] - statusPriority[b.status];
+
+ if (statusComparison !== 0) return statusComparison;
+ }
+
+ return startedAtSortFn(a, b);
+ };
+
+ const sortFn = settingsStore.isConcurrencyEnabled ? statusThenStartedAtSortFn : startedAtSortFn;
+
const currentExecutions = computed(() => {
const data = Object.values(currentExecutionsById.value);
- data.sort((a, b) => {
- return new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime();
- });
+ data.sort(sortFn);
return data;
});
diff --git a/packages/editor-ui/src/stores/settings.store.ts b/packages/editor-ui/src/stores/settings.store.ts
index 7d881b74d4..822c05f4dc 100644
--- a/packages/editor-ui/src/stores/settings.store.ts
+++ b/packages/editor-ui/src/stores/settings.store.ts
@@ -70,6 +70,8 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
const concurrency = computed(() => settings.value.concurrency);
+ const isConcurrencyEnabled = computed(() => concurrency.value !== -1);
+
const isPublicApiEnabled = computed(() => api.value.enabled);
const isSwaggerUIEnabled = computed(() => api.value.swaggerUi.enabled);
@@ -384,6 +386,7 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
security,
nodeJsVersion,
concurrency,
+ isConcurrencyEnabled,
isPublicApiEnabled,
isSwaggerUIEnabled,
isPreviewMode,