fix(frontend): schedule operator perms + add instance settings in operator menu (#4912)

* fix(frontend): schedule operator perms + add instance settings in operator menu

* fix npm timeout

* forgot
This commit is contained in:
HugoCasa
2024-12-12 18:11:44 +01:00
committed by GitHub
parent c0c26e0715
commit 91ad9f11fd
18 changed files with 242 additions and 149 deletions
+2 -3
View File
@@ -15,7 +15,6 @@ jobs:
with:
node-version: 18
- name: "npm check"
timeout-minutes: 2
run:
cd frontend && npm ci && npm run generate-backend-client && npm run
timeout-minutes: 5
run: cd frontend && npm ci && npm run generate-backend-client && npm run
check
@@ -7,14 +7,16 @@
export let placement: 'bottom-end' | 'top-end' = 'bottom-end'
export let color: 'nord' | 'dark' = 'dark'
export let disabled = false
</script>
<Popup
floatingConfig={{ strategy: 'absolute', placement: placement }}
containerClasses="border rounded-lg shadow-lg p-4 bg-surface"
{disabled}
>
<svelte:fragment slot="button">
<Button {color} size="xs" nonCaptureEvent={true}>
<Button {color} size="xs" nonCaptureEvent={true} {disabled}>
<div class="flex flex-row gap-1 items-center"
><Pen size={14} /> Custom tags&nbsp;<Tooltip light
>Tags are assigned to scripts and flows. Workers only accept jobs that correspond to their
@@ -16,6 +16,7 @@
export let items: Item[] | (() => Item[]) | (() => Promise<Item[]>) = []
export let justifyEnd: boolean = true
export let disabled = false
async function computeItems(): Promise<Item[]> {
if (typeof items === 'function') {
@@ -26,7 +27,7 @@
}
</script>
<Menu placement="bottom-end" {justifyEnd} on:close on:open>
<Menu placement="bottom-end" {justifyEnd} on:close on:open {disabled}>
<div slot="trigger">
{#if $$slots.buttonReplacement}
<slot name="buttonReplacement" />
+2 -1
View File
@@ -1105,6 +1105,7 @@
}
}
let timeoutModel: NodeJS.Timeout | undefined = undefined
async function loadMonaco() {
try {
console.log("Loading Monaco's language client")
@@ -1143,7 +1144,6 @@
// updateEditorKeybindingsMode(editor, 'vim', undefined)
let timeoutModel: NodeJS.Timeout | undefined = undefined
let ataModel: NodeJS.Timeout | undefined = undefined
editor?.onDidChangeModelContent((event) => {
@@ -1316,6 +1316,7 @@
sqlSchemaCompletor && sqlSchemaCompletor.dispose()
copilotCompletor && copilotCompletor.dispose()
sqlTypeCompletor && sqlTypeCompletor.dispose()
timeoutModel && clearTimeout(timeoutModel)
})
async function genRoot(hostname: string) {
@@ -191,8 +191,8 @@
<div>
<Tabs bind:selected={handlerSelected} class="mt-2 mb-4">
<Tab value="slack">Slack</Tab>
<Tab value="custom">
<Tab value="slack" disabled={!isEditable}>Slack</Tab>
<Tab value="custom" disabled={!isEditable}>
Custom
<slot name="custom-tab-tooltip" />
</Tab>
@@ -208,7 +208,7 @@
allowFlow={true}
bind:scriptPath={handlerPath}
bind:itemKind={customHandlerKind}
allowRefresh
allowRefresh={isEditable}
/>
{#if handlerPath === undefined}
@@ -217,6 +217,7 @@
color="dark"
size="xs"
href={customScriptTemplate}
disabled={!isEditable}
target="_blank">Create from template</Button
>
{/if}
@@ -218,11 +218,13 @@
async function loadScript(p: string | undefined): Promise<void> {
if (p) {
runnable = undefined
if (is_flow) {
runnable = await FlowService.getFlowByPath({ workspace: $workspaceStore!, path: p })
} else {
runnable = await ScriptService.getScriptByPath({ workspace: $workspaceStore!, path: p })
}
try {
if (is_flow) {
runnable = await FlowService.getFlowByPath({ workspace: $workspaceStore!, path: p })
} else {
runnable = await ScriptService.getScriptByPath({ workspace: $workspaceStore!, path: p })
}
} catch (err) {}
} else {
runnable = undefined
}
@@ -524,7 +526,11 @@
<Drawer size="900px" bind:this={drawer}>
<DrawerContent
title={edit ? `Edit schedule ${initialPath}` : 'New schedule'}
title={edit
? can_write
? `Edit schedule ${initialPath}`
: `View schedule ${initialPath}`
: 'New schedule'}
on:close={drawer.closeDrawer}
>
<svelte:fragment slot="actions">
@@ -541,22 +547,24 @@
View runs
</Button>
</div>
<div class="mr-8 center-center -mt-1">
<Toggle
disabled={!can_write}
checked={enabled}
options={{ right: 'Enabled' }}
on:change={async (e) => {
await ScheduleService.setScheduleEnabled({
path: initialPath,
workspace: $workspaceStore ?? '',
requestBody: { enabled: e.detail }
})
dispatch('update')
sendUserToast(`${e.detail ? 'enabled' : 'disabled'} schedule ${initialPath}`)
}}
/>
</div>
{#if can_write}
<div class="mr-8 center-center -mt-1">
<Toggle
disabled={!can_write}
checked={enabled}
options={{ right: 'Enabled' }}
on:change={async (e) => {
await ScheduleService.setScheduleEnabled({
path: initialPath,
workspace: $workspaceStore ?? '',
requestBody: { enabled: e.detail }
})
dispatch('update')
sendUserToast(`${e.detail ? 'enabled' : 'disabled'} schedule ${initialPath}`)
}}
/>
</div>
{/if}
{/if}
<Button
startIcon={{ icon: Save }}
@@ -565,7 +573,8 @@
emptyString(script_path) ||
(errorHandlerSelected == 'slack' &&
!emptyString(errorHandlerPath) &&
emptyString(errorHandlerExtraArgs['channel']))}
emptyString(errorHandlerExtraArgs['channel'])) ||
!can_write}
on:click={scheduleScript}
>
{edit ? 'Save' : 'Schedule'}
@@ -575,6 +584,7 @@
{#if drawerLoading}
<Loader2 class="animate-spin" />
{:else}
{can_write}
<div class="flex flex-col gap-12">
<div class="flex flex-col gap-4">
<div>
@@ -587,6 +597,7 @@
placeholder="Short summary to be displayed when listed"
class="text-sm w-full"
bind:value={summary}
disabled={!can_write}
on:keyup={() => {
if (!edit && summary?.length > 0 && !dirtyPath) {
pathC?.setName(
@@ -664,6 +675,7 @@
size="xs"
bind:checked={isLatestCron}
on:change={onVersionChange}
disabled={!can_write}
/>
</div>
{/if}
@@ -682,6 +694,7 @@
}}
bind:checked={showPauseUntil}
size="xs"
disabled={!can_write}
/>
{#if showPauseUntil}
<DateTimeInput bind:value={paused_until} />
@@ -715,6 +728,8 @@
scriptPath={script_path}
allowFlow={true}
{itemKind}
allowView={script_path != '' && !!runnable}
allowEdit={script_path != '' && !!runnable && !$userStore?.operator}
/>
{/if}
{#if itemKind == 'flow'}
@@ -752,6 +767,11 @@
This {is_flow ? 'flow' : 'script'} takes no argument
</div>
{/if}
{:else if script_path != ''}
<div class="text-xs texg-gray-700 my-2">
You cannot see the the {is_flow ? 'flow' : 'script'} input form as you do not have
access to it.
</div>
{:else}
<div class="text-xs texg-gray-700 my-2">
Pick a {is_flow ? 'flow' : 'script'} and fill its argument here
@@ -787,6 +807,7 @@
<div class="flex flex-row items-center gap-1 text-2xs text-tertiary">
defaults
<Dropdown
disabled={!can_write}
items={[
{
displayName: `Override future schedules only`,
@@ -879,6 +900,7 @@
</div>
</Section>
{:else if optionTabSelected === 'recovery_handler'}
{@const disabled = !can_write || emptyString($enterpriseLicense)}
<Section label="Recovery handler">
<svelte:fragment slot="header">
<div class="flex flex-row gap-2">
@@ -890,6 +912,7 @@
<div class="flex flex-row items-center text-tertiary text-2xs gap-2">
defaults
<Dropdown
{disabled}
items={[
{
displayName: `Override future schedules only`,
@@ -910,7 +933,7 @@
</div>
</svelte:fragment>
<ErrorOrRecoveryHandler
isEditable={can_write && !emptyString($enterpriseLicense)}
isEditable={!disabled}
errorOrRecovery="recovery"
bind:handlerSelected={recoveryHandlerSelected}
bind:handlerPath={recoveryHandlerPath}
@@ -963,12 +986,14 @@
class="!w-14 mx-2 text-center"
bind:value={recoveredTimes}
min="1"
{disabled}
/>
<p>time{recoveredTimes > 1 ? 's in a row' : ''}</p>
</div>
</div>
</Section>
{:else if optionTabSelected === 'success_handler'}
{@const disabled = !can_write || emptyString($enterpriseLicense)}
<Section label="Success handler">
<svelte:fragment slot="header">
<div class="flex flex-row gap-2">
@@ -980,6 +1005,7 @@
<div class="flex flex-row items-center text-tertiary text-2xs gap-2">
defaults
<Dropdown
{disabled}
items={[
{
displayName: `Override future schedules only`,
@@ -1000,7 +1026,7 @@
</div>
</svelte:fragment>
<ErrorOrRecoveryHandler
isEditable={can_write && !emptyString($enterpriseLicense)}
isEditable={!disabled}
errorOrRecovery="success"
bind:handlerSelected={successHandlerSelected}
bind:handlerPath={successHandlerPath}
@@ -1031,6 +1057,7 @@
</ErrorOrRecoveryHandler>
</Section>
{:else if optionTabSelected === 'retries'}
{@const disabled = !can_write || emptyString($enterpriseLicense)}
<Section label="Retries">
<svelte:fragment slot="header">
<div class="flex flex-row gap-2">
@@ -1045,14 +1072,17 @@
flow step in the flow editor.
</Tooltip>
</svelte:fragment>
<FlowRetries bind:flowModuleRetry={retry} disabled={itemKind !== 'script'} />
<FlowRetries
bind:flowModuleRetry={retry}
disabled={itemKind !== 'script' || disabled}
/>
</Section>
{:else if optionTabSelected === 'tag'}
<Section
label="Custom script tag"
tooltip="When set, the script tag will be overridden by this tag"
>
<WorkerTagPicker bind:tag popupPlacement="top-end" />
<WorkerTagPicker bind:tag popupPlacement="top-end" disabled={!can_write} />
</Section>
{/if}
{:else}
@@ -220,8 +220,8 @@
) {
scriptEditor?.disableCollaboration()
script.content = initialCode(language, kind, template)
scriptEditor?.inferSchema(script.content, language)
if (script.content != editor?.getCode() && editor?.getScriptLang() == language) {
scriptEditor?.inferSchema(script.content, language, true)
if (script.content != editor?.getCode()) {
setCode(script.content)
}
}
@@ -128,7 +128,7 @@
let hasPreprocessor = false
export async function inferSchema(code: string, nlang?: SupportedLanguage) {
export async function inferSchema(code: string, nlang?: SupportedLanguage, resetArgs = false) {
let nschema = schema ?? emptySchema()
try {
@@ -142,6 +142,9 @@
(selectedTab === 'preprocessor' ? !result?.no_main_func : result?.has_preprocessor) ?? false
validCode = true
if (resetArgs) {
args = {}
}
schema = nschema
} catch (e) {
validCode = false
+41 -34
View File
@@ -28,6 +28,7 @@
export let disabled = false
export let allowRefresh = false
export let allowEdit = true
export let allowView = true
let items: { value: string; label: string }[] = []
let drawerViewer: Drawer
@@ -138,16 +139,18 @@
href="{base}/flows/edit/{scriptPath}">Edit</Button
>
{/if}
<Button
color="light"
size="xs"
variant="border"
on:click={async () => {
drawerFlowViewer.openDrawer()
}}
>
View
</Button>
{#if allowView}
<Button
color="light"
size="xs"
variant="border"
on:click={async () => {
drawerFlowViewer.openDrawer()
}}
>
View
</Button>
{/if}
</div>
{:else if itemKind == 'app'}
<div class="flex gap-2">
@@ -163,16 +166,18 @@
Edit
</Button>
{/if}
<Button
color="light"
size="xs"
variant="border"
target="_blank"
startIcon={{ icon: Code }}
href="{base}/apps/get/{scriptPath}"
>
View
</Button>
{#if allowView}
<Button
color="light"
size="xs"
variant="border"
target="_blank"
startIcon={{ icon: Code }}
href="{base}/apps/get/{scriptPath}"
>
View
</Button>
{/if}
</div>
{:else}
<div class="flex gap-2">
@@ -188,20 +193,22 @@
Edit
</Button>
{/if}
<Button
color="light"
size="xs"
variant="border"
startIcon={{ icon: Code }}
on:click={async () => {
const { language, content } = await getScriptByPath(scriptPath ?? '')
code = content
lang = language
drawerViewer.openDrawer()
}}
>
View
</Button>
{#if allowView}
<Button
color="light"
size="xs"
variant="border"
startIcon={{ icon: Code }}
on:click={async () => {
const { language, content } = await getScriptByPath(scriptPath ?? '')
code = content
lang = language
drawerViewer.openDrawer()
}}
>
View
</Button>
{/if}
</div>
{/if}
{/if}
@@ -8,6 +8,7 @@
export let tag: string | undefined
export let popupPlacement: 'bottom-end' | 'top-end' = 'bottom-end'
export let disabled = false
loadWorkerGroups()
async function loadWorkerGroups() {
@@ -21,7 +22,7 @@
<div class="max-w-sm grow">
{#if $workerTags}
{#if $workerTags?.length ?? 0 > 0}
<WorkerTagSelect noLabel bind:tag />
<WorkerTagSelect noLabel bind:tag {disabled} />
{:else}
<div class="text-sm text-secondary flex flex-row gap-2">
No custom worker group tag defined on this instance in "Workers {'->'} Custom tags"
@@ -50,6 +51,7 @@
loadWorkerGroups()
}}
startIcon={{ icon: RotateCw }}
{disabled}
/>
<AssignableTags placement={popupPlacement} color="nord" />
<AssignableTags placement={popupPlacement} color="nord" {disabled} />
</div>
@@ -5,6 +5,7 @@
export let tag: string | undefined
export let noLabel: boolean = false
export let nullTag: string | undefined = undefined
export let disabled = false
loadWorkerGroups()
@@ -28,6 +29,7 @@
tag = undefined
}
}}
{disabled}
>
{#if tag}
<option value="">reset to default</option>
@@ -11,6 +11,7 @@
export let justifyEnd: boolean = false
export let lightMode: boolean = false
export let maxHeight: number = 900
export let disabled = false
const [floatingRef, floatingContent] = createFloatingActions({
strategy: 'fixed',
middleware: [offset(), flip(), shift()],
@@ -18,10 +19,10 @@
})
</script>
<Menu let:open as="div" class="relative hover:z-50 flex w-full h-8">
<Menu let:open as="div" class="relative hover:z-50 flex w-full h-8">
<ResolveOpen {open} on:open on:close />
<div use:floatingRef class="w-full">
<MenuButton class={twMerge('w-full', justifyEnd ? 'flex justify-end' : '')}>
<MenuButton class={twMerge('w-full', justifyEnd ? 'flex justify-end' : '')} disabled={disabled}>
<slot name="trigger" />
</MenuButton>
</div>
@@ -18,10 +18,11 @@
export let noTransition = false
export let popupHover = false
export let preventPopupClosingOnClickInside = false
export let disabled = false
</script>
<Popover on:close class="leading-none">
<PopoverButton let:open>
<PopoverButton let:open {disabled}>
<div use:floatingRef>
<slot name="button" {open} />
</div>
@@ -10,14 +10,15 @@
Code2,
LayoutDashboard,
Building,
Calendar
Calendar,
ServerCog
} from 'lucide-svelte'
import { base } from '$lib/base'
import Menu from '../common/menu/MenuV2.svelte'
import MultiplayerMenu from './MultiplayerMenu.svelte'
import { enterpriseLicense } from '$lib/stores'
import { enterpriseLicense, superadmin } from '$lib/stores'
import MenuButton from './MenuButton.svelte'
import { MenuItem } from '@rgossiaux/svelte-headlessui'
import MenuLink from './MenuLink.svelte'
@@ -43,10 +44,6 @@
]
let secondMenuLinks = [
{
label: 'Schedules',
href: `${base}/schedules`
},
{
label: 'Resources',
href: `${base}/resources`
@@ -55,6 +52,18 @@
label: 'Variables',
href: `${base}/variables`
},
{
label: 'Custom HTTP Routes',
href: `${base}/routes`
},
{
label: 'Websocket Triggers',
href: `${base}/websockets`
},
{
label: 'Kafka Triggers',
href: `${base}/kafka`
},
{
label: 'Audit Logs',
href: `${base}/audit_logs`
@@ -158,6 +167,20 @@
All Workspaces
</a>
{#if $superadmin}
<MenuItem
href="#superadmin-settings"
class={twMerge(
'flex flex-row gap-3.5 items-center px-2 py-2 ',
'text-secondary text-xs',
'hover:bg-surface-hover hover:text-primary cursor-pointer'
)}
>
<ServerCog size={14} />
Instance settings
</MenuItem>
{/if}
<MenuItem
href="#"
on:click={() => logout()}
@@ -151,7 +151,7 @@
on:close={drawer.closeDrawer}
>
<svelte:fragment slot="actions">
{#if !drawerLoading && can_write}
{#if !drawerLoading}
{#if edit}
<div class="mr-8 center-center -mt-1">
<Toggle
@@ -169,21 +169,23 @@
/>
</div>
{/if}
<Button
startIcon={{ icon: Save }}
disabled={pathError != '' ||
emptyString(script_path) ||
emptyString(kafka_resource_path) ||
topics.length < 1 ||
topics.some((t) => emptyString(t)) ||
topicsError != '' ||
emptyString(group_id) ||
groupIdError != '' ||
!can_write}
on:click={updateTrigger}
>
Save
</Button>
{#if can_write}
<Button
startIcon={{ icon: Save }}
disabled={pathError != '' ||
emptyString(script_path) ||
emptyString(kafka_resource_path) ||
topics.length < 1 ||
topics.some((t) => emptyString(t)) ||
topicsError != '' ||
emptyString(group_id) ||
groupIdError != '' ||
!can_write}
on:click={updateTrigger}
>
Save
</Button>
{/if}
{/if}
</svelte:fragment>
{#if drawerLoading}
@@ -300,7 +302,8 @@
allowFlow={true}
bind:itemKind
bind:scriptPath={script_path}
allowRefresh
allowRefresh={can_write}
allowEdit={!$userStore?.operator}
/>
</div>
</Section>
@@ -21,6 +21,8 @@
import JsonEditor from '../apps/editor/settingsPanel/inputEditor/JsonEditor.svelte'
import FileUpload from '../common/fileUpload/FileUpload.svelte'
import SimpleEditor from '../SimpleEditor.svelte'
import { json } from 'svelte-highlight/languages'
import { Highlight } from 'svelte-highlight'
let is_flow: boolean = false
let initialPath = ''
@@ -341,36 +343,48 @@
{#if static_asset_config}
<div class="flex flex-col w-full gap-1">
<Toggle
class="flex justify-end"
bind:checked={s3FileUploadRawMode}
size="xs"
options={{ left: 'Existing file' }}
/>
{#if s3FileUploadRawMode}
<JsonEditor
bind:editor={s3Editor}
on:focus={(e) => {
dispatch('focus')
}}
on:blur={(e) => {
dispatch('blur')
}}
code={JSON.stringify(static_asset_config ?? { s3: '' }, null, 2)}
bind:value={static_asset_config}
/>
<Button
variant="border"
color="light"
{#if can_write}
<Toggle
class="flex justify-end"
bind:checked={s3FileUploadRawMode}
size="xs"
btnClasses="mt-1"
on:click={() => {
s3FilePicker?.open?.(static_asset_config)
}}
startIcon={{ icon: Pipette }}
>
Choose an object from the catalog
</Button>
options={{ left: 'Existing file' }}
disabled={!can_write}
/>
{/if}
{#if s3FileUploadRawMode}
{#if can_write}
<JsonEditor
bind:editor={s3Editor}
on:focus={(e) => {
dispatch('focus')
}}
on:blur={(e) => {
dispatch('blur')
}}
code={JSON.stringify(static_asset_config ?? { s3: '' }, null, 2)}
bind:value={static_asset_config}
/>
{:else}
<Highlight
language={json}
code={JSON.stringify(static_asset_config ?? { s3: '' }, null, 2)}
/>
{/if}
{#if can_write}
<Button
variant="border"
color="light"
size="xs"
btnClasses="mt-1"
on:click={() => {
s3FilePicker?.open?.(static_asset_config)
}}
startIcon={{ icon: Pipette }}
>
Choose an object from the catalog
</Button>
{/if}
{:else}
<FileUpload
allowMultiple={false}
@@ -403,7 +417,8 @@
allowFlow={true}
bind:itemKind
bind:scriptPath={script_path}
allowRefresh
allowRefresh={can_write}
allowEdit={!$userStore?.operator}
/>
{#if script_path === undefined}
@@ -253,7 +253,7 @@
on:close={drawer.closeDrawer}
>
<svelte:fragment slot="actions">
{#if !drawerLoading && can_write}
{#if !drawerLoading}
{#if edit}
<div class="mr-8 center-center -mt-1">
<Toggle
@@ -273,17 +273,19 @@
/>
</div>
{/if}
<Button
startIcon={{ icon: Save }}
disabled={pathError != '' ||
urlError != '' ||
invalidInitialMessages ||
emptyString(script_path) ||
!can_write}
on:click={updateTrigger}
>
Save
</Button>
{#if can_write}
<Button
startIcon={{ icon: Save }}
disabled={pathError != '' ||
urlError != '' ||
invalidInitialMessages ||
emptyString(script_path) ||
!can_write}
on:click={updateTrigger}
>
Save
</Button>
{/if}
{/if}
</svelte:fragment>
{#if drawerLoading}
@@ -411,7 +413,8 @@
allowFlow={true}
bind:itemKind
bind:scriptPath={script_path}
allowRefresh
allowRefresh={can_write}
allowEdit={!$userStore?.operator}
/>
</div>
</Section>
@@ -384,10 +384,10 @@
<Button
on:click={() => scheduleEditor?.openEdit(path, is_flow)}
size="xs"
startIcon={{ icon: Pen }}
startIcon={{ icon: canWrite ? Pen : Eye }}
color="dark"
>
Edit
{canWrite ? 'Edit' : 'View'}
</Button>
<Dropdown
items={[
@@ -412,9 +412,8 @@
}
},
{
displayName: 'Edit',
icon: Pen,
disabled: !canWrite,
displayName: canWrite ? 'Edit' : 'View',
icon: canWrite ? Pen : Eye,
action: () => {
scheduleEditor?.openEdit(path, is_flow)
}