GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — [web-console] Let Start save unsaved changes and schedule the start · feldera/feldera@8972c2d

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
  • js-packages/web-console
    • src/lib
      • components
        • common
          • SplitButton.svelte
        • pipelines
          • editor
            • ActionsFixture.svelte
          • list
            • Actions.svelte
          • table
            • AvailableActions.svelte
      • compositions
        • usePipelineAction.svelte.ts
        • useWritablePipeline.svelte.ts
      • functions
        • pipelineStatusColor.spec.ts
      • services
        • pipelineManager.spec.ts
        • pipelineManager.ts
    • tests
      • pastedLink.e2e.ts
      • startUnsavedChanges.e2e.ts
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎js-packages/web-console/src/lib/components/common/SplitButton.svelte‎

Copy file name to clipboardExpand all lines: js-packages/web-console/src/lib/components/common/SplitButton.svelte
+4Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
size = 'md',
4646
variant = 'filled',
4747
toggleClass = '',
48+
disabled = false,
4849
class: _class = ''
4950
}: {
5051
/** The main action. Its button's size class should match `size`. */
@@ -58,6 +59,8 @@
5859
variant?: SplitButtonVariant
5960
/** Classes for the chevron only, typically the colour preset of the main button. */
6061
toggleClass?: string
62+
/** Disables the chevron. Disable the main button in `children` separately. */
63+
disabled?: boolean
6164
class?: string
6265
} = $props()
6366
@@ -74,6 +77,7 @@
7477
type="button"
7578
class="btn-split-toggle btn-icon {toggleButtonClass}"
7679
onclick={ontoggle}
80+
{disabled}
7781
aria-label={toggleLabel}
7882
>
7983
<span class="fd fd-chevron-down {toggleClasses[size].icon}"></span>
Collapse file

‎js-packages/web-console/src/lib/components/pipelines/editor/ActionsFixture.svelte‎

Copy file name to clipboardExpand all lines: js-packages/web-console/src/lib/components/pipelines/editor/ActionsFixture.svelte
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,5 @@
3131
{deleted}
3232
editConfigDisabled={deleted}
3333
unsavedChanges={false}
34-
saveFile={() => {}}
34+
saveFile={async () => true}
3535
></Actions>
Collapse file

‎js-packages/web-console/src/lib/components/pipelines/list/Actions.svelte‎

Copy file name to clipboardExpand all lines: js-packages/web-console/src/lib/components/pipelines/list/Actions.svelte
+79-63Lines changed: 79 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,10 @@ groups related actions into multi-action dropdowns when multiple options are ava
6666
import { getPipelineAction } from '#lib/compositions/usePipelineAction.svelte.ts'
6767
import { usePipelineManager } from '#lib/compositions/usePipelineManager.svelte.ts'
6868
import { useToast } from '#lib/compositions/useToastNotification.ts'
69-
import type { WritablePipeline } from '#lib/compositions/useWritablePipeline.svelte.ts'
69+
import {
70+
settlePendingPatches,
71+
type WritablePipeline
72+
} from '#lib/compositions/useWritablePipeline.svelte.ts'
7073
import {
7174
deletePipelineDisabledReason,
7275
editSettingsDisabledReason,
@@ -96,7 +99,8 @@ groups related actions into multi-action dropdowns when multiple options are ava
9699
deleted?: boolean
97100
unsavedChanges: boolean
98101
onActionSuccess?: (pipelineName: string, action: PipelineAction) => void
99-
saveFile: () => void
102+
/** Saves all unsaved files. Resolves to `false` if the user cancels the save. */
103+
saveFile: () => Promise<boolean>
100104
class?: string
101105
} = $props()
102106
@@ -206,18 +210,19 @@ groups related actions into multi-action dropdowns when multiple options are ava
206210
207211
// Helper function to get raw actions for current pipeline status
208212
const getRawActions = (status: typeof pipeline.current.status) => {
213+
const startable: (keyof typeof actions)[] = [
214+
'_start',
215+
'_start_paused',
216+
'_standby',
217+
'_storage_indicator',
218+
'_saveFile',
219+
'_configurations',
220+
'_more'
221+
]
209222
return filterByPermission(
210223
match(status)
211224
.returnType<(keyof typeof actions)[]>()
212-
.with('Stopped', () => [
213-
'_start',
214-
'_start_paused',
215-
'_standby',
216-
'_storage_indicator',
217-
'_saveFile',
218-
'_configurations',
219-
'_more'
220-
])
225+
.with('Stopped', () => startable)
221226
.with('Preparing', 'Provisioning', 'Initializing', () => [
222227
'_kill',
223228
'_spinner',
@@ -318,22 +323,24 @@ groups related actions into multi-action dropdowns when multiple options are ava
318323
{ CompilingSql: P.any },
319324
{ SqlCompiled: P.any },
320325
{ CompilingRust: P.any },
321-
(cause) => [
322-
...(Object.values(cause)[0].cause === 'upgrade' ? ['_unschedule' as const] : []),
323-
'_start_pending',
324-
'_storage_indicator',
325-
'_saveFile',
326-
'_configurations',
327-
'_more'
328-
]
326+
(status) =>
327+
Object.values(status)[0].cause === 'start'
328+
? [
329+
'_unschedule',
330+
'_start_pending',
331+
'_storage_indicator',
332+
'_saveFile',
333+
'_configurations',
334+
'_more'
335+
]
336+
: startable
337+
)
338+
// Unsaved changes may fix the error, so Start saves them and schedules a start.
339+
.with('SqlError', 'RustError', 'SystemError', () =>
340+
unsavedChanges
341+
? startable
342+
: ['_start_error', '_storage_indicator', '_saveFile', '_configurations', '_more']
329343
)
330-
.with('SqlError', 'RustError', 'SystemError', () => [
331-
'_start_error',
332-
'_storage_indicator',
333-
'_saveFile',
334-
'_configurations',
335-
'_more'
336-
])
337344
.exhaustive()
338345
)
339346
}
@@ -419,11 +426,24 @@ groups related actions into multi-action dropdowns when multiple options are ava
419426
const { postPipelineAction } = getPipelineAction()
420427
const pipelineActionCallbacks = usePipelineActionCallbacks()
421428
429+
// Save the code and wait for its PATCH requests before the start request, because
430+
// pipeline-manager rejects code changes after a start request. Resolves to `false`
431+
// if the user cancels the save or a PATCH request fails.
432+
const saveBeforeStart = async () =>
433+
(await saveFile()) &&
434+
settlePendingPatches().then(
435+
() => true,
436+
() => false
437+
)
438+
422439
const performStartAction = async (
423440
action: PipelineAction,
424441
pipelineName: string
425442
// nextStatus: PipelineStatus
426443
) => {
444+
if (!(await saveBeforeStart())) {
445+
return
446+
}
427447
const callbacks =
428448
action === 'start'
429449
? {
@@ -479,8 +499,7 @@ groups related actions into multi-action dropdowns when multiple options are ava
479499
const pipelineName = pipeline.current.name
480500
performStartAction('start', pipelineName)
481501
},
482-
disabled: () => unsavedChanges,
483-
disabledText: 'Save First',
502+
disabled: () => false,
484503
standaloneButton: _start
485504
},
486505
_start_paused: {
@@ -490,8 +509,7 @@ groups related actions into multi-action dropdowns when multiple options are ava
490509
const pipelineName = pipeline.current.name
491510
performStartAction('start_paused', pipelineName)
492511
},
493-
disabled: () => unsavedChanges,
494-
disabledText: 'Save First',
512+
disabled: () => false,
495513
standaloneButton: _start_paused
496514
},
497515
_resume: {
@@ -501,8 +519,7 @@ groups related actions into multi-action dropdowns when multiple options are ava
501519
const pipelineName = pipeline.current.name
502520
performStartAction('resume', pipelineName)
503521
},
504-
disabled: () => unsavedChanges,
505-
disabledText: 'Save First',
522+
disabled: () => false,
506523
standaloneButton: _start
507524
},
508525
_stop: {
@@ -537,16 +554,11 @@ groups related actions into multi-action dropdowns when multiple options are ava
537554
_standby: {
538555
label: 'Start in Standby',
539556
description: 'Put the pipeline in standby mode',
540-
onclick: async () => {
557+
onclick: () => {
541558
const pipelineName = pipeline.current.name
542-
const { waitFor } = await postPipelineAction(pipelineName, 'standby')
543-
waitFor().then(
544-
(shouldContinue) => shouldContinue && onActionSuccess?.(pipelineName, 'standby'),
545-
toastError('Waiting for pipeline to standby')
546-
)
559+
performStartAction('standby', pipelineName)
547560
},
548-
disabled: () => unsavedChanges,
549-
disabledText: 'Save First',
561+
disabled: () => false,
550562
standaloneButton: _standby
551563
},
552564
_activate: {
@@ -813,59 +825,56 @@ groups related actions into multi-action dropdowns when multiple options are ava
813825
{#snippet _start()}
814826
{@render start({
815827
text: 'Start',
816-
action: 'start',
817-
disabled: unsavedChanges
828+
action: 'start'
818829
})}
819-
{#if unsavedChanges}
820-
<Tooltip placement="top">Save the program before running</Tooltip>
821-
{/if}
822830
{/snippet}
823831
{#snippet _start_paused()}
824832
{@render start({
825833
text: 'Start',
826-
action: 'start_paused',
827-
disabled: unsavedChanges
834+
action: 'start_paused'
828835
})}
829-
{#if unsavedChanges}
830-
<Tooltip placement="top">Save the program before running</Tooltip>
831-
{/if}
832836
{/snippet}
833837
{#snippet _resume()}
834838
{@render start({
835839
text: 'Resume',
836-
action: 'resume',
837-
disabled: unsavedChanges
840+
action: 'resume'
838841
})}
839-
{#if unsavedChanges}
840-
<Tooltip placement="top">Save the program before running</Tooltip>
841-
{/if}
842842
{/snippet}
843843
{#snippet _standby()}
844844
{@render start({
845845
text: 'Standby',
846-
action: 'standby',
847-
disabled: unsavedChanges
846+
action: 'standby'
848847
})}
849848
<Tooltip placement="top">Put the pipeline in standby mode</Tooltip>
850849
{/snippet}
851850
{#snippet _activate()}
852851
{@render start({
853852
text: 'Activate',
854-
action: 'activate',
855-
disabled: unsavedChanges
853+
action: 'activate'
856854
})}
857855
<Tooltip placement="top">Activate the pipeline to start data ingress and processing</Tooltip>
858856
{/snippet}
857+
<!-- A split button, so that the disabled Start has the layout of the enabled one. -->
859858
{#snippet _start_disabled()}
860-
{@render start({ text: 'Start', disabled: true })}
859+
{@const config = multiActionConfigs.start}
860+
<SplitButton
861+
ontoggle={() => {}}
862+
toggleLabel={config.ariaLabel}
863+
toggleClass={config.buttonClass}
864+
variant={config.variant}
865+
size={config.size}
866+
disabled
867+
>
868+
{@render start({ text: 'Start', disabled: true })}
869+
</SplitButton>
861870
{/snippet}
862871
{#snippet _start_error()}
863872
{@render _start_disabled()}
864873
<Tooltip placement="top">Resolve errors before running</Tooltip>
865874
{/snippet}
866875
{#snippet _start_pending()}
867876
{@render _start_disabled()}
868-
<Tooltip placement="top">Wait for compilation to complete</Tooltip>
877+
<Tooltip placement="top">The pipeline starts after compilation</Tooltip>
869878
{/snippet}
870879
{#snippet _pause()}
871880
<button
@@ -961,8 +970,15 @@ groups related actions into multi-action dropdowns when multiple options are ava
961970
<!-- TODO: add support for a short size when in mobile -->
962971
<button
963972
class="{buttonClass} {longClass} {basicBtnColor}"
964-
onclick={() => {
965-
globalDialog.dialog = killDialog
973+
onclick={async () => {
974+
// The pipeline does not run before the compilation ends, so the kill needs no
975+
// confirmation.
976+
const pipelineName = pipeline.current.name
977+
const { waitFor } = await postPipelineAction(pipelineName, 'kill')
978+
waitFor().then(
979+
(shouldContinue) => shouldContinue && onActionSuccess?.(pipelineName, 'kill'),
980+
toastError('Waiting for the pipeline start to cancel')
981+
)
966982
}}
967983
>
968984
Cancel start
Collapse file

‎js-packages/web-console/src/lib/components/pipelines/table/AvailableActions.svelte‎

Copy file name to clipboardExpand all lines: js-packages/web-console/src/lib/components/pipelines/table/AvailableActions.svelte
+4-5Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,10 @@
5656
{ CompilingSql: P.any },
5757
{ SqlCompiled: P.any },
5858
{ CompilingRust: P.any },
59-
(cause) => [
60-
...(Object.values(cause)[0].cause === 'upgrade' ? ['kill' as const] : []),
61-
...storageAction,
62-
'delete'
63-
]
59+
(status) =>
60+
Object.values(status)[0].cause === 'start'
61+
? ['kill', ...storageAction, 'delete']
62+
: ['start', ...storageAction, 'delete']
6463
)
6564
.with('Stopped', () => ['start', ...storageAction, 'delete'])
6665
.with('Preparing', 'Provisioning', 'Initializing', () => ['kill', 'delete'])
Collapse file

‎js-packages/web-console/src/lib/compositions/usePipelineAction.svelte.ts‎

Copy file name to clipboardExpand all lines: js-packages/web-console/src/lib/compositions/usePipelineAction.svelte.ts
+27-4Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
import type { NamesInUnion } from 'common-lib/union'
22
import { unionName } from 'common-lib/union'
33
import invariant from 'tiny-invariant'
4-
import { match } from 'ts-pattern'
4+
import { match, P } from 'ts-pattern'
55
import { hasSelectedRelations } from '#lib/components/pipelines/editor/TabChangeStream.svelte'
66
import type {
7-
ExtendedPipeline,
87
PipelineAction,
98
PipelineStatus,
109
PipelineThumb
1110
} from '#lib/services/pipelineManager.ts'
1211
import { page } from '$app/state'
1312
import { usePipelineList, useUpdatePipelineList } from './pipelines/usePipelineList.svelte'
14-
import { type PipelineManagerApi, usePipelineManager } from './usePipelineManager.svelte'
13+
import { usePipelineManager } from './usePipelineManager.svelte'
1514
import { useReactiveWaiter } from './useReactiveWaiter.svelte'
1615

1716
let postPipelineAction: (
@@ -97,9 +96,33 @@ export const usePipelineAction = () => {
9796
.with('approve_changes', () => undefined)
9897
.exhaustive()
9998

99+
// A start or a stop does not end a compilation. For a compiling pipeline, the
100+
// optimistic status keeps the compiling state and only sets whether a start is
101+
// scheduled.
102+
const compilingStatus = (status: PipelineStatus) => {
103+
const cause = match(action)
104+
.returnType<'start' | 'compile' | undefined>()
105+
.with('start', 'start_paused', 'standby', () => 'start')
106+
.with('stop', 'kill', () => 'compile')
107+
.otherwise(() => undefined)
108+
if (!cause) {
109+
return undefined
110+
}
111+
return match(status)
112+
.returnType<PipelineStatus | undefined>()
113+
.with({ Queued: P.any }, () => ({ Queued: { cause } }))
114+
.with({ CompilingSql: P.any }, () => ({ CompilingSql: { cause } }))
115+
.with({ SqlCompiled: P.any }, () => ({ SqlCompiled: { cause } }))
116+
.with({ CompilingRust: P.any }, () => ({ CompilingRust: { cause } }))
117+
.otherwise(() => undefined)
118+
}
119+
100120
// Apply optimistic updates
101121
if (optimisticStatus) {
102-
updatePipeline(pipeline_name, (p) => ({ ...p, status: optimisticStatus }))
122+
updatePipeline(pipeline_name, (p) => ({
123+
...p,
124+
status: compilingStatus(p.status) ?? optimisticStatus
125+
}))
103126
} else if (action === 'clear') {
104127
updatePipeline(pipeline_name, (p) => ({ ...p, storageStatus: 'Clearing' }))
105128
}
Collapse file

‎js-packages/web-console/src/lib/compositions/useWritablePipeline.svelte.ts‎

Copy file name to clipboardExpand all lines: js-packages/web-console/src/lib/compositions/useWritablePipeline.svelte.ts
+11-1Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ import {
55
} from '#lib/compositions/usePipelineManager.svelte.ts'
66
import type { ExtendedPipeline, Pipeline, PipelineThumb } from '#lib/services/pipelineManager.ts'
77

8+
// Module-level because `writablePipeline` is re-created whenever the pipeline changes.
9+
const pendingPatches = new Set<Promise<unknown>>()
10+
11+
/** Wait for every pipeline patch in flight. Rejects if one of them fails. */
12+
export const settlePendingPatches = () => Promise.all(pendingPatches)
13+
814
export const writablePipeline = ({
915
api,
1016
pipeline,
@@ -31,7 +37,11 @@ export const writablePipeline = ({
3137
if (optimistic) {
3238
update(newPipeline)
3339
}
34-
const res = await api.patchPipeline(pipeline.current.name, newPipeline)
40+
const request = api.patchPipeline(pipeline.current.name, newPipeline)
41+
pendingPatches.add(request)
42+
const forget = () => pendingPatches.delete(request)
43+
request.then(forget, forget)
44+
const res = await request
3545
if (!optimistic) {
3646
set(res)
3747
}

0 commit comments

Comments
0 (0)
You are visitor 01402520 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.