) => {
+ ;(locator.element() as HTMLButtonElement).click()
+}
+
describe('CheckpointDialog.svelte', () => {
describe('A. Content', () => {
it('renders the title', async () => {
@@ -31,14 +35,14 @@ describe('CheckpointDialog.svelte', () => {
it('calls onConfirm when Checkpoint is clicked', async () => {
const onConfirm = vi.fn()
await render(CheckpointDialog, { onConfirm })
- await page.getByTestId('btn-confirm-checkpoint').click()
+ clickButton(page.getByTestId('btn-confirm-checkpoint'))
expect(onConfirm).toHaveBeenCalledOnce()
})
it('does not call onConfirm when Cancel is clicked', async () => {
const onConfirm = vi.fn()
await render(CheckpointDialog, { onConfirm })
- await page.getByTestId('btn-dialog-cancel').click()
+ clickButton(page.getByTestId('btn-dialog-cancel'))
expect(onConfirm).not.toHaveBeenCalled()
})
})
diff --git a/js-packages/web-console/src/lib/components/pipelines/list/Actions.svelte b/js-packages/web-console/src/lib/components/pipelines/list/Actions.svelte
index 14c95abdd03..448a2251f97 100644
--- a/js-packages/web-console/src/lib/components/pipelines/list/Actions.svelte
+++ b/js-packages/web-console/src/lib/components/pipelines/list/Actions.svelte
@@ -49,9 +49,14 @@ groups related actions into multi-action dropdowns when multiple options are ava
import DeleteDialog, { deleteDialogProps } from '$lib/components/dialogs/DeleteDialog.svelte'
import JSONDialog from '$lib/components/dialogs/JSONDialog.svelte'
import PipelineConfigurationsPopup from '$lib/components/layout/pipelines/PipelineConfigurationsPopup.svelte'
+ import { duplicatePipeline, duplicatePipelineTooltip } from '$lib/compositions/duplicatePipeline'
import { useGlobalDialog } from '$lib/compositions/layout/useGlobalDialog.svelte'
import { useIsMobile } from '$lib/compositions/layout/useIsMobile.svelte'
import { usePipelineActionCallbacks } from '$lib/compositions/pipelines/usePipelineActionCallbacks.svelte'
+ import {
+ usePipelineList,
+ useUpdatePipelineList
+ } from '$lib/compositions/pipelines/usePipelineList.svelte'
import { getPipelineAction } from '$lib/compositions/usePipelineAction.svelte'
import { usePipelineManager } from '$lib/compositions/usePipelineManager.svelte'
import { usePremiumFeatures } from '$lib/compositions/usePremiumFeatures.svelte'
@@ -84,11 +89,26 @@ groups related actions into multi-action dropdowns when multiple options are ava
const globalDialog = useGlobalDialog()
const api = usePipelineManager()
+ const pipelineList = usePipelineList()
+ const { discardPendingListRefresh, updatePipeline, updatePipelines } = useUpdatePipelineList()
const deletePipeline = async (pipelineName: string) => {
await api.deletePipeline(pipelineName)
onDeletePipeline?.(pipelineName)
goto(resolve('/'))
}
+ const duplicateCurrentPipeline = async () => {
+ const pipelines = pipelineList.pipelines
+ if (!pipelines || unsavedChanges) {
+ return
+ }
+
+ const newPipeline = await duplicatePipeline(api, pipeline.current, pipelines, {
+ discardPendingListRefresh,
+ updatePipeline,
+ updatePipelines
+ })
+ await goto(resolve(`/pipelines/${encodeURIComponent(newPipeline.name)}/`))
+ }
const { toastError } = useToast()
const actions = {
@@ -105,7 +125,8 @@ groups related actions into multi-action dropdowns when multiple options are ava
_stop,
_multiStop,
_multiStart,
- _delete,
+ _duplicate,
+ _more,
_spacer_short,
_spacer_long,
_spinner,
@@ -135,7 +156,7 @@ groups related actions into multi-action dropdowns when multiple options are ava
'_saveFile',
'_configurations',
'_storage_indicator',
- '_delete'
+ '_more'
])
.with('Preparing', 'Provisioning', 'Initializing', () => [
'_kill',
@@ -143,7 +164,7 @@ groups related actions into multi-action dropdowns when multiple options are ava
'_saveFile',
'_configurations',
'_storage_indicator',
- '_delete'
+ '_more'
])
.with('Pausing', 'Resuming', () => [
...stopButtons,
@@ -151,7 +172,7 @@ groups related actions into multi-action dropdowns when multiple options are ava
'_saveFile',
'_configurations',
'_storage_indicator',
- '_delete'
+ '_more'
])
.with('Unavailable', () => [
...stopButtons,
@@ -159,7 +180,7 @@ groups related actions into multi-action dropdowns when multiple options are ava
'_saveFile',
'_configurations',
'_storage_indicator',
- '_delete'
+ '_more'
])
.with('Running', () => [
...stopButtons,
@@ -167,7 +188,7 @@ groups related actions into multi-action dropdowns when multiple options are ava
'_saveFile',
'_configurations',
'_storage_indicator',
- '_delete'
+ '_more'
])
.with('Paused', () => [
...stopButtons,
@@ -175,7 +196,7 @@ groups related actions into multi-action dropdowns when multiple options are ava
'_saveFile',
'_configurations',
'_storage_indicator',
- '_delete'
+ '_more'
])
.with('Suspending', () => [
'_kill',
@@ -183,7 +204,7 @@ groups related actions into multi-action dropdowns when multiple options are ava
'_saveFile',
'_configurations',
'_storage_indicator',
- '_delete'
+ '_more'
])
.with('Suspended', () => [
'_spinner',
@@ -191,7 +212,7 @@ groups related actions into multi-action dropdowns when multiple options are ava
'_saveFile',
'_configurations',
'_storage_indicator',
- '_delete'
+ '_more'
])
.with('Standby', () => [
'_kill',
@@ -199,7 +220,7 @@ groups related actions into multi-action dropdowns when multiple options are ava
'_saveFile',
'_configurations',
'_storage_indicator',
- '_delete'
+ '_more'
])
.with('Bootstrapping', () => [
'_kill',
@@ -207,7 +228,7 @@ groups related actions into multi-action dropdowns when multiple options are ava
'_saveFile',
'_configurations',
'_storage_indicator',
- '_delete'
+ '_more'
])
.with('Replaying', () => [
'_kill',
@@ -215,14 +236,14 @@ groups related actions into multi-action dropdowns when multiple options are ava
'_saveFile',
'_configurations',
'_storage_indicator',
- '_delete'
+ '_more'
])
.with('AwaitingApproval', () => [
'_kill',
'_saveFile',
'_configurations',
'_storage_indicator',
- '_delete'
+ '_more'
])
.with('Stopping', () => [
'_kill',
@@ -230,7 +251,7 @@ groups related actions into multi-action dropdowns when multiple options are ava
'_saveFile',
'_configurations',
'_storage_indicator',
- '_delete'
+ '_more'
])
.with(
{ Queued: P.any },
@@ -243,7 +264,7 @@ groups related actions into multi-action dropdowns when multiple options are ava
'_saveFile',
'_configurations',
'_storage_indicator',
- '_delete'
+ '_more'
]
)
.with('SqlError', 'RustError', 'SystemError', () => [
@@ -251,7 +272,7 @@ groups related actions into multi-action dropdowns when multiple options are ava
'_saveFile',
'_configurations',
'_storage_indicator',
- '_delete'
+ '_more'
])
.exhaustive()
}
@@ -288,7 +309,7 @@ groups related actions into multi-action dropdowns when multiple options are ava
const active = $derived.by(() => {
const rawActions = getRawActions(pipeline.current.status)
// Group actions into dropdowns
- return processActionsForDropdowns(rawActions)
+ return [...processActionsForDropdowns(rawActions), '_duplicate' as const]
})
function processActionsForDropdowns(
@@ -637,18 +658,52 @@ groups related actions into multi-action dropdowns when multiple options are ava
{/snippet} -->
-{#snippet _delete()}
-
-
-
- {#if editConfigDisabled}
- Stop the pipeline to delete it
- {/if}
+{#snippet _more()}
+
+ {#snippet trigger(toggle)}
+
+ {/snippet}
+ {#snippet content(close)}
+
+
+
+ {/snippet}
+
+{/snippet}
+{#snippet _duplicate()}
+
+
+ {#if unsavedChanges}
+ Save the program before duplicating.
+ {:else}
+ {duplicatePipelineTooltip}
+ {/if}
+
{/snippet}
{#snippet start({
text,
diff --git a/js-packages/web-console/src/lib/components/pipelines/table/AvailableActions.svelte b/js-packages/web-console/src/lib/components/pipelines/table/AvailableActions.svelte
index 43f410c7c2d..cac7709db28 100644
--- a/js-packages/web-console/src/lib/components/pipelines/table/AvailableActions.svelte
+++ b/js-packages/web-console/src/lib/components/pipelines/table/AvailableActions.svelte
@@ -2,6 +2,7 @@
import { join } from 'array-join'
import { match, P } from 'ts-pattern'
import DeleteDialog, { deleteDialogProps } from '$lib/components/dialogs/DeleteDialog.svelte'
+ import { duplicatePipeline, duplicatePipelineTooltip } from '$lib/compositions/duplicatePipeline'
import { useGlobalDialog } from '$lib/compositions/layout/useGlobalDialog.svelte'
import { useUpdatePipelineList } from '$lib/compositions/pipelines/usePipelineList.svelte'
import { getPipelineAction } from '$lib/compositions/usePipelineAction.svelte'
@@ -15,9 +16,10 @@
pipelines,
selectedPipelines = $bindable()
}: { pipelines: PipelineThumb[]; selectedPipelines: string[] } = $props()
- const { updatePipeline } = useUpdatePipelineList()
+ const { discardPendingListRefresh, updatePipeline, updatePipelines } = useUpdatePipelineList()
const sortedSelectedPipelines = $derived([...selectedPipelines].sort())
const availableActions = [
+ 'duplicate' as const,
'start' as const,
'resume' as const,
'pause' as const,
@@ -26,12 +28,14 @@
'delete' as const,
'clear' as const
]
+ type AvailableAction = (typeof availableActions)[number]
+ type StatusAction = Exclude
const isPremium = usePremiumFeatures()
const stop = isPremium.value ? ['stop' as const] : []
const statusActions = ({ status, storageStatus }: (typeof selected)[number]) => {
const storageAction = storageStatus === 'InUse' ? ['clear' as const] : []
return match(status)
- .returnType<(typeof availableActions)[number][]>()
+ .returnType()
.with(
{ Queued: P.any },
{ CompilingSql: P.any },
@@ -69,7 +73,7 @@
(_, p) => p
)
)
- const eligibleFor = (action: (typeof availableActions)[number]) =>
+ const eligibleFor = (action: StatusAction) =>
selected
.filter((pipeline) => statusActions(pipeline).includes(action))
.map((p) => p.name)
@@ -78,11 +82,15 @@
if (selected.length === 0) {
return []
}
- const supportedByAny = new Set(selected.flatMap(statusActions))
+ const supportedByAny = new Set(selected.flatMap(statusActions))
+ if (selected.length === 1) {
+ supportedByAny.add('duplicate')
+ }
return availableActions
.filter((action) => supportedByAny.has(action))
.map((action) =>
match(action)
+ .with('duplicate', () => btnDuplicate)
.with('start', () => btnStart)
.with('resume', () => btnResume)
.with('pause', () => btnPause)
@@ -96,7 +104,7 @@
const globalDialog = useGlobalDialog()
const api = usePipelineManager()
- const postPipelinesAction = (action: Exclude<(typeof availableActions)[number], 'delete'>) => {
+ const postPipelinesAction = (action: Exclude) => {
selected
.filter((pipeline) => statusActions(pipeline).includes(action))
.forEach((pipeline) => {
@@ -128,6 +136,20 @@
})
selectedPipelines = []
}
+
+ const duplicateSelectedPipeline = async () => {
+ const pipeline = selected[0]
+ if (!pipeline) {
+ return
+ }
+
+ await duplicatePipeline(api, pipeline, pipelines, {
+ discardPendingListRefresh,
+ updatePipeline,
+ updatePipelines
+ })
+ selectedPipelines = []
+ }
{#snippet btnStart()}
@@ -166,6 +188,16 @@
Delete
{/snippet}
+{#snippet btnDuplicate()}
+
+{/snippet}
{#snippet btnClear()}