Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@
"@storybook/addon-essentials": "^8.6.11",
"@storybook/addon-interactions": "^8.6.11",
"@storybook/addon-links": "^8.6.11",
"@storybook/addon-svelte-csf": "^5.0.0-next.23",
"@storybook/addon-svelte-csf": "^5.0.10",
"@storybook/addon-themes": "^8.6.11",
"@storybook/blocks": "^8.6.11",
"@storybook/icons": "^1.4.0",
Expand Down
2 changes: 1 addition & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 3 additions & 10 deletions src/lib/components/payload-input-with-encoding.svelte
Original file line number Diff line number Diff line change
@@ -1,19 +1,12 @@
<script context="module" lang="ts">
const encoding = ['json/plain', 'json/protobuf'] as const;
export type PayloadInputEncoding = (typeof encoding)[number];
export const isPayloadInputEncodingType = (
x: unknown,
): x is PayloadInputEncoding => encoding.includes(x as PayloadInputEncoding);
</script>

<script lang="ts">
import { type Writable } from 'svelte/store';
import type { Writable } from 'svelte/store';

import Card from '$lib/holocene/card.svelte';
import Input from '$lib/holocene/input/input.svelte';
import RadioGroup from '$lib/holocene/radio-input/radio-group.svelte';
import RadioInput from '$lib/holocene/radio-input/radio-input.svelte';
import { translate } from '$lib/i18n/translate';
import type { PayloadInputEncoding } from '$lib/models/payload-encoding';

import PayloadInput from './payload-input.svelte';

Expand Down Expand Up @@ -44,7 +37,7 @@
<div class="flex w-full flex-col gap-2">
<RadioGroup
description={translate('workflows.encoding')}
bind:group={encoding}
group={encoding}
name="encoding"
>
<RadioInput id="json/plain" value="json/plain" label="json/plain" />
Expand Down
3 changes: 1 addition & 2 deletions src/lib/components/schedule/schedule-form-view.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import Link from '$lib/holocene/link.svelte';
import Loading from '$lib/holocene/loading.svelte';
import { translate } from '$lib/i18n/translate';
import type { PayloadInputEncoding } from '$lib/models/payload-encoding';
import { error, loading } from '$lib/stores/schedules';
import {
customSearchAttributes,
Expand All @@ -31,8 +32,6 @@
} from '$lib/utilities/route-for';
import { writeActionsAreAllowed } from '$lib/utilities/write-actions-are-allowed';

import type { PayloadInputEncoding } from '../payload-input-with-encoding.svelte';

import ScheduleInputPayload from './schedule-input-payload.svelte';
import SchedulesSearchAttributesInputs from './schedules-search-attributes-inputs.svelte';

Expand Down
9 changes: 5 additions & 4 deletions src/lib/components/schedule/schedule-input-payload.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@

import Button from '$lib/holocene/button.svelte';
import { translate } from '$lib/i18n/translate';
import {
isPayloadInputEncodingType,
type PayloadInputEncoding,
} from '$lib/models/payload-encoding';
import type { Payloads } from '$lib/types';
import { atob } from '$lib/utilities/atob';
import { getSinglePayload } from '$lib/utilities/encode-payload';

import PayloadDecoder from '../event/payload-decoder.svelte';
import PayloadInputWithEncoding, {
isPayloadInputEncodingType,
type PayloadInputEncoding,
} from '../payload-input-with-encoding.svelte';
import PayloadInputWithEncoding from '../payload-input-with-encoding.svelte';

export let input: string;
export let editInput: boolean;
Expand Down
274 changes: 274 additions & 0 deletions src/lib/components/standalone-activity-form/form.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
<script lang="ts">
import { writable } from 'svelte/store';

import { onDestroy } from 'svelte';
import { superForm } from 'sveltekit-superforms';
import { zodClient } from 'sveltekit-superforms/adapters';
import { twMerge } from 'tailwind-merge';
import z from 'zod/v3';

import { page } from '$app/state';

import Button from '$lib/holocene/button.svelte';
import Card from '$lib/holocene/card.svelte';
import DurationInput from '$lib/holocene/duration-input/duration-input.svelte';
import Input from '$lib/holocene/input/input.svelte';
import Label from '$lib/holocene/label.svelte';
import MarkdownEditor from '$lib/holocene/markdown-editor/markdown-editor.svelte';
import { translate } from '$lib/i18n/translate';
import {
encodings,
type PayloadInputEncoding,
} from '$lib/models/payload-encoding';
import { startStandaloneActivity } from '$lib/services/standalone-activities';
import { getIdentity } from '$lib/utilities/core-context';

import type { StandaloneActivityFormData } from './types';
import Message from '../form/message.svelte';
import PayloadInputWithEncoding from '../payload-input-with-encoding.svelte';
import AddSearchAttributes from '../workflow/add-search-attributes.svelte';

interface Props {
namespace: string;
}

const { namespace }: Props = $props();

const formDefaults = $derived<StandaloneActivityFormData>({
namespace,
identity: getIdentity(),
activityId: page.url.searchParams.get('activityId') ?? '',
activityType: page.url.searchParams.get('activityType') ?? '',
taskQueue: page.url.searchParams.get('taskQueue') ?? '',
startToCloseTimeout: page.url.searchParams.get('startToCloseTimeout') ?? '',
});

// https://svelte.dev/docs/svelte/compiler-warnings#state_referenced_locally
const getFormDefaults = () => formDefaults;

const schema = z
.object({
identity: z.string(),
namespace: z.string(),
activityId: z.string().min(1, { message: 'Activity ID is required.' }),
taskQueue: z.string().min(1, { message: 'Task Queue is required.' }),
activityType: z
.string()
.min(1, { message: 'Activity Type is required.' }),
input: z.string().optional(),
startToCloseTimeout: z.string().optional(),
scheduleToCloseTimeout: z.string().optional(),
encoding: z.enum(encodings).optional(),
messageType: z.string().optional(),
searchAttributes: z
.array(
z.object({ label: z.string(), value: z.string(), type: z.string() }),
)
.optional(),
summary: z.string().optional(),
details: z.string().optional(),
scheduleToStartTimeout: z.string().optional(),
heartbeatTimeout: z.number().optional(),
retryPolicy: z
.object({
stuff: z.string(),
})
.optional(),
})
.superRefine((data, context) => {
if (!data.startToCloseTimeout && !data.scheduleToCloseTimeout) {
context.addIssue({
code: z.ZodIssueCode.custom,
path: ['startToCloseTimeout'],
message:
'Either "Start to Close Timeout" or "Schedule to Close Timeout" is required.',
});
context.addIssue({
code: z.ZodIssueCode.custom,
path: ['scheduleToCloseTimeout'],
message:
'Either "Start to Close Timeout" or "Schedule to Close Timeout" is required.',
});
}
});

const { form, enhance, errors, message } = superForm(
{
...getFormDefaults(),
input: '',
encoding: '',
messageType: '',
scheduleToCloseTimeout: '',
scheduleToStartTimeout: '',
searchAttributes: [],
summary: '',
details: '',
},
{
SPA: true,
dataType: 'json',
resetForm: false,
invalidateAll: false,
validators: zodClient(schema),
onUpdate: async ({ form }) => {
if (!form.valid) return;

try {
startStandaloneActivity(form.data);
return { type: 'success' };
} catch (error) {
console.error(error);
return {
type: 'error',
};
}
},
},
);

$inspect($form);

const encoding = writable<PayloadInputEncoding>('json/plain');
let advancedOptionsVisible = $state(false);

const unsubscribe = encoding.subscribe((e) => {
$form.encoding = e;
});

onDestroy(() => {
unsubscribe?.();
});

const generateRandomId = () => {
$form.activityId = crypto.randomUUID();
};
</script>

<form class="max-w-[45rem] space-y-4" use:enhance novalidate>
<Message value={$message} />

<Input
class="grow"
label="Activity ID"
required
id="activityId"
bind:value={$form.activityId}
error={!!$errors?.activityId}
hintText={$errors?.activityId?.[0]}
>
<Button
class="ml-2.5"
variant="secondary"
slot="after-input"
on:click={generateRandomId}
leadingIcon="retry">Random UUID</Button
>
</Input>

<Input
id="taskQueue"
required
label="Task Queue"
bind:value={$form.taskQueue}
error={!!$errors.taskQueue}
hintText={$errors.taskQueue?.[0]}
/>
<Input
id="activityType"
required
label="Activity Type"
bind:value={$form.activityType}
error={!!$errors.activityType}
hintText={$errors.activityType?.[0]}
/>

<PayloadInputWithEncoding
bind:input={$form.input}
bind:messageType={$form.messageType}
{encoding}
/>

<Card
class={twMerge(
'space-y-4',
$errors.startToCloseTimeout ? 'border-danger' : '',
)}
>
<p class="text-base font-medium">Activity Timeouts</p>

<DurationInput
id="startToCloseTimeout"
label="Start to Close Timeout"
required={!$form.scheduleToCloseTimeout}
hintText="Maximum time an activity is allowed to execute after being picked up by a worker."
bind:value={$form.startToCloseTimeout}
/>

<DurationInput
id="scheduleToCloseTimeout"
label="Schedule to Close Timeout"
required={!$form.startToCloseTimeout}
hintText="How long the caller is willing to wait for an activity completion."
bind:value={$form.scheduleToCloseTimeout}
/>

<DurationInput
id="scheduleToStartTimeout"
label="Schedule to Start Timeout"
hintText={'Limits time an activity task can stay in a task queue before a worker picks it up. Defaults to "Schedule to Close Timeout" if not specified.'}
bind:value={$form.scheduleToStartTimeout}
/>

{#if $errors.startToCloseTimeout}
<p class="text-xs text-danger">
{$errors.startToCloseTimeout}
</p>
{/if}
</Card>

{#if advancedOptionsVisible}
<Card class="space-y-4">
<div class="space-y-2">
<p class="text-base font-medium">Custom Search Attributes</p>
<p class="text-secondary">
Indexed fields used in a List Filter to filter a list of Standalone
Activities.
</p>
</div>
<AddSearchAttributes bind:attributesToAdd={$form.searchAttributes} />
</Card>

<Card class="space-y-4">
<div class="space-y-2">
<p class="text-base font-medium">User Metadata</p>
<p class="text-secondary">
Add context to Standalone Activities to help identify and understand
its operations.
</p>
</div>
<div class="space-y-2">
<Label label={translate('workflows.summary')} for="summary" />
<MarkdownEditor bind:content={$form.summary} />
</div>
<div class="space-y-2">
<Label label={translate('workflows.details')} for="details" />
<MarkdownEditor bind:content={$form.details} />
</div>
</Card>
{/if}

<div class="flex items-center justify-between">
<Button
type="button"
variant="ghost"
trailingIcon={advancedOptionsVisible ? 'chevron-up' : 'chevron-down'}
on:click={() => (advancedOptionsVisible = !advancedOptionsVisible)}
>
{translate('common.more-options')}
</Button>

<Button type="submit">
{translate('activities.start-standalone-activity')}
</Button>
</div>
</form>
19 changes: 19 additions & 0 deletions src/lib/components/standalone-activity-form/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { PayloadInputEncoding } from '$lib/models/payload-encoding';
import type { SearchAttributeInput } from '$lib/stores/search-attributes';

export interface StandaloneActivityFormData {
identity: string;
namespace: string;
activityId: string;
taskQueue: string;
activityType: string;
startToCloseTimeout: string;
scheduleToCloseTimeout?: string;
scheduleToStartTimeout?: string;
input?: string;
encoding?: PayloadInputEncoding;
messageType?: string;
searchAttributes?: SearchAttributeInput[];
summary?: string;
details?: string;
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
<script lang="ts">
import { writable, type Writable } from 'svelte/store';

import PayloadInputWithEncoding, {
type PayloadInputEncoding,
} from '$lib/components/payload-input-with-encoding.svelte';
import PayloadInputWithEncoding from '$lib/components/payload-input-with-encoding.svelte';
import Input from '$lib/holocene/input/input.svelte';
import Modal from '$lib/holocene/modal.svelte';
import Option from '$lib/holocene/select/option.svelte';
import Select from '$lib/holocene/select/select.svelte';
import { translate } from '$lib/i18n/translate';
import type { PayloadInputEncoding } from '$lib/models/payload-encoding';
import { signalWorkflow } from '$lib/services/workflow-service';
import { toaster } from '$lib/stores/toaster';
import { workflowRun } from '$lib/stores/workflow-run';
Expand Down
Loading