Skip to content

Commit 5c20f32

Browse files
feat(triggers): redesigned triggers page (#16717)
1 parent 38fc9e1 commit 5c20f32

23 files changed

Lines changed: 676 additions & 817 deletions

File tree

ui/packages/design-system/src/assets/styles/ks-tokens.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
--ks-radius-sm: #{v.$border-radius-sm};
2626
--ks-radius-base: #{v.$border-radius};
2727
--ks-radius-lg: #{v.$border-radius-lg};
28+
--ks-radius-xl: #{v.$border-radius-xl};
2829

2930
// shadow
3031
--ks-shadow-sm: #{v.$box-shadow-sm};

ui/packages/design-system/src/assets/styles/variables.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,6 @@ $colors: (
5050
// border radius
5151
$border-radius: 0.5rem !default;
5252
$border-radius-lg: 0.625rem !default;
53+
$border-radius-xl: 0.875rem !default;
5354
$border-radius-sm: 0.375rem !default;
5455
$border-radius-xs: 0.25rem !default;

ui/packages/design-system/src/components/Feedback/KsDialog.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
.kel-dialog {
6767
--kel-dialog-bg-color: var(--ks-bg-elevated);
6868
border: 1px solid var(--ks-border-default);
69+
border-radius: var(--ks-radius-xl);
6970
7071
.kel-dialog__header {
7172
font-size: var(--ks-font-size-base);
@@ -97,7 +98,8 @@
9798
padding-right: var(--kel-dialog-padding-primary);
9899
width: calc(100% + var(--kel-dialog-padding-primary) * 2);
99100
background-color: var(--ks-bg-base);
100-
border-radius: 0 0 var(--kel-dialog-border-radius) var(--kel-dialog-border-radius);
101+
border-bottom-left-radius: var(--ks-radius-xl);
102+
border-bottom-right-radius: var(--ks-radius-xl);
101103
}
102104
}
103105
</style>

ui/packages/design-system/src/components/Form/KsSearch.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,11 @@
5151
width: 100%;
5252
5353
.kel-input__wrapper {
54+
height: 32px;
5455
gap: 8px;
5556
border-radius: 8px;
5657
background-color: var(--ks-bg-input);
57-
border: 1px solid var(--ks-border-subtle);
58+
border: 1px solid var(--ks-border-strong);
5859
box-shadow: 0px 1px 4px 0px var(--ks-shadow-element);
5960
transition: border-color 0.2s ease;
6061
Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
<template>
2+
<KsDialog
3+
v-model="visible"
4+
width="500px"
5+
destroyOnClose
6+
appendToBody
7+
>
8+
<template #header>
9+
<div class="header">
10+
<div class="title">
11+
<h2>{{ $t("add") }} {{ displayName }}</h2>
12+
<KsTag v-if="trigger.ee" type="warning" size="small">
13+
EE
14+
</KsTag>
15+
</div>
16+
<code class="type">{{ trigger.type }}</code>
17+
</div>
18+
</template>
19+
20+
<div class="modal-body">
21+
<KsTabs v-model="activeTab" type="box">
22+
<KsTabPane name="form" :label="$t('triggers_add_modal_tab_form')">
23+
<div class="form">
24+
<KsForm labelPosition="left" labelWidth="122px" :model="formModel">
25+
<KsFormItem :label="$t('namespace')" required>
26+
<NamespaceSelect
27+
v-model="formModel.namespace"
28+
:placeholder="$t('triggers_add_modal_namespace_placeholder')"
29+
:clearable="false"
30+
:autoDefault="false"
31+
@update:model-value="onNamespaceChange"
32+
/>
33+
</KsFormItem>
34+
35+
<KsFormItem :label="$t('flow')" required>
36+
<KsSelect
37+
v-model="formModel.flowId"
38+
filterable
39+
:placeholder="$t('triggers_add_modal_flow_placeholder')"
40+
:disabled="!formModel.namespace"
41+
:loading="flowsLoading"
42+
>
43+
<KsOption v-for="f in flowOptions" :key="f.id" :label="f.id" :value="f.id" />
44+
</KsSelect>
45+
</KsFormItem>
46+
47+
<KsFormItem :label="$t('triggers_add_modal_trigger_id')" required>
48+
<KsInput
49+
v-model="formModel.triggerId"
50+
class="id-input"
51+
:placeholder="$t('triggers_add_modal_trigger_id_placeholder')"
52+
/>
53+
</KsFormItem>
54+
</KsForm>
55+
56+
<p class="hint">
57+
{{ $t("triggers_add_modal_properties_hint") }}
58+
</p>
59+
</div>
60+
</KsTabPane>
61+
62+
<KsTabPane name="source" :label="$t('triggers_add_modal_tab_source')">
63+
<div class="source">
64+
<KsIconButton
65+
class="copy"
66+
:aria-label="copied ? $t('copied') : $t('copy')"
67+
@click="copySource"
68+
>
69+
<CheckIcon v-if="copied" class="text-success" />
70+
<ContentCopy v-else />
71+
</KsIconButton>
72+
<KsEditor
73+
v-bind="editorBindings"
74+
:modelValue="sourceYaml"
75+
lang="yaml"
76+
:inline="true"
77+
:navbar="false"
78+
readOnly
79+
:options="{
80+
fullHeight: false,
81+
customHeight: 24,
82+
editor: {
83+
padding: {top: 6, bottom: 6},
84+
guides: {indentation: false},
85+
},
86+
}"
87+
/>
88+
</div>
89+
</KsTabPane>
90+
91+
<KsTabPane name="documentation" :label="$t('triggers_add_modal_tab_documentation')">
92+
<div class="docs">
93+
<PluginDocumentation
94+
v-if="documentationPlugin"
95+
:plugin="documentationPlugin"
96+
fetchPluginDocumentation
97+
/>
98+
<KsSkeleton v-else :rows="6" animated />
99+
</div>
100+
</KsTabPane>
101+
</KsTabs>
102+
</div>
103+
104+
<template #footer>
105+
<div class="footer">
106+
<KsButton @click="$emit('cancel')">
107+
{{ $t("cancel") }}
108+
</KsButton>
109+
<KsButton type="primary" :disabled="!canSubmit" @click="addTriggerToFlow">
110+
+ {{ $t("triggers_add_modal_add_button") }}
111+
</KsButton>
112+
</div>
113+
</template>
114+
</KsDialog>
115+
</template>
116+
117+
<script setup lang="ts">
118+
import {computed, ref, watch} from "vue"
119+
import {useRouter} from "vue-router"
120+
121+
import {KsEditor} from "@kestra-io/design-system"
122+
import CheckIcon from "vue-material-design-icons/Check.vue"
123+
import ContentCopy from "vue-material-design-icons/ContentCopy.vue"
124+
125+
import {useFlowStore} from "../../../stores/flow"
126+
import {usePluginsStore, type TriggerPluginDto, type PluginComponent} from "../../../stores/plugins"
127+
import {useTriggerDraftStore} from "../../../stores/triggerDraft"
128+
import {useEditorBindings} from "../../../composables/useEditorBindings"
129+
import {triggerDisplayName} from "./triggerCatalog"
130+
131+
import NamespaceSelect from "../../namespaces/components/NamespaceSelect.vue"
132+
import PluginDocumentation from "../../plugins/PluginDocumentation.vue"
133+
134+
const visible = defineModel<boolean>("visible", {required: true})
135+
const props = defineProps<{trigger: TriggerPluginDto}>()
136+
defineEmits<{cancel: []}>()
137+
138+
const COPY_FEEDBACK_MS = 1600
139+
const TAB_VALUES = ["form", "source", "documentation"] as const
140+
type TabValue = typeof TAB_VALUES[number];
141+
142+
const router = useRouter()
143+
const flowStore = useFlowStore()
144+
const pluginsStore = usePluginsStore()
145+
const triggerDraftStore = useTriggerDraftStore()
146+
147+
const editorBindings = useEditorBindings()
148+
149+
const activeTab = ref<TabValue>("form")
150+
const copied = ref(false)
151+
const flowsLoading = ref(false)
152+
153+
const flowOptions = ref<{id: string; namespace: string}[]>([])
154+
const documentationPlugin = ref<PluginComponent | null>(null)
155+
156+
const generateId = () => `mytrigger_${Math.floor(10000 + Math.random() * 90000)}`
157+
const formModel = ref({
158+
namespace: "",
159+
flowId: "",
160+
triggerId: generateId(),
161+
})
162+
163+
const displayName = computed(() => triggerDisplayName(props.trigger))
164+
const canSubmit = computed(() =>
165+
!!formModel.value.namespace && !!formModel.value.flowId && !!formModel.value.triggerId.trim(),
166+
)
167+
168+
const getTriggerId = () => formModel.value.triggerId.trim() || "mytrigger"
169+
const sourceYaml = computed(() => ` - id: ${getTriggerId()}\n type: ${props.trigger.type}`)
170+
171+
const loadFlows = async (namespace: string) => {
172+
if (!namespace) {
173+
flowOptions.value = []
174+
return
175+
}
176+
flowsLoading.value = true
177+
try {
178+
const response = await flowStore.findFlows({namespace, size: 200, sort: "id:asc"})
179+
flowOptions.value = (response?.results ?? []).map((f: any) => ({id: f.id, namespace: f.namespace}))
180+
} finally {
181+
flowsLoading.value = false
182+
}
183+
}
184+
185+
const onNamespaceChange = (ns: string | string[] | undefined) => {
186+
formModel.value.flowId = ""
187+
loadFlows(typeof ns === "string" ? ns : "")
188+
}
189+
190+
const copySource = async () => {
191+
await navigator.clipboard.writeText(`triggers:\n${sourceYaml.value}\n`)
192+
copied.value = true
193+
setTimeout(() => copied.value = false, COPY_FEEDBACK_MS)
194+
}
195+
196+
const loadDocumentation = async () => {
197+
try {
198+
const doc = await pluginsStore.load({cls: props.trigger.type, commit: false})
199+
documentationPlugin.value = {...doc, cls: props.trigger.type}
200+
} catch {
201+
documentationPlugin.value = null
202+
}
203+
}
204+
205+
const addTriggerToFlow = () => {
206+
if (!canSubmit.value) return
207+
208+
triggerDraftStore.setDraft({
209+
namespace: formModel.value.namespace,
210+
flowId: formModel.value.flowId,
211+
triggerYaml: `id: ${getTriggerId()}\ntype: ${props.trigger.type}\n`,
212+
})
213+
214+
visible.value = false
215+
router.push({
216+
name: "flows/update",
217+
params: {namespace: formModel.value.namespace, id: formModel.value.flowId, tab: "edit"},
218+
query: {createTrigger: "true"},
219+
})
220+
}
221+
222+
watch(visible, val => {
223+
if (val) {
224+
activeTab.value = "form"
225+
copied.value = false
226+
formModel.value = {namespace: "", flowId: "", triggerId: generateId()}
227+
loadDocumentation()
228+
}
229+
}, {immediate: true})
230+
</script>
231+
232+
<style scoped lang="scss">
233+
.header {
234+
.title {
235+
display: flex;
236+
align-items: center;
237+
gap: var(--ks-spacing-2);
238+
239+
h2 {
240+
min-width: 0;
241+
margin: 0;
242+
overflow: hidden;
243+
font-size: var(--ks-font-size-lg);
244+
font-weight: var(--ks-font-weight-bold);
245+
color: var(--ks-text-primary);
246+
white-space: nowrap;
247+
text-overflow: ellipsis;
248+
}
249+
}
250+
251+
.type {
252+
display: block;
253+
margin-top: var(--ks-spacing-1);
254+
overflow: hidden;
255+
font-size: var(--ks-font-size-xs);
256+
font-family: var(--ks-font-family-mono);
257+
color: var(--ks-text-secondary);
258+
white-space: nowrap;
259+
text-overflow: ellipsis;
260+
}
261+
}
262+
263+
.modal-body :deep(.kel-tabs--box) {
264+
.kel-tabs__header {
265+
margin: var(--ks-spacing-3) 0 var(--ks-spacing-5);
266+
}
267+
268+
.kel-tabs__nav-wrap {
269+
background: transparent;
270+
border-bottom: none;
271+
}
272+
273+
.kel-tabs__nav {
274+
width: fit-content;
275+
background: var(--ks-tabs-bg, #08090A);
276+
border-radius: var(--ks-radius-lg);
277+
}
278+
}
279+
280+
.form {
281+
:deep(.kel-form) {
282+
--kel-color-danger: var(--ks-text-error);
283+
}
284+
285+
:deep(.kel-form-item):first-of-type {
286+
margin-top: 0;
287+
}
288+
289+
:deep(.kel-form-item__label) {
290+
font-weight: var(--ks-font-weight-semibold);
291+
}
292+
293+
:deep(.id-input .kel-input__inner) {
294+
font-size: var(--ks-font-size-xs);
295+
font-weight: var(--ks-font-weight-regular);
296+
}
297+
298+
.hint {
299+
margin: var(--ks-spacing-4) 0;
300+
font-size: var(--ks-font-size-2xs);
301+
color: var(--ks-text-secondary);
302+
}
303+
}
304+
305+
.source {
306+
position: relative;
307+
308+
.copy {
309+
position: absolute;
310+
top: var(--ks-spacing-2);
311+
right: var(--ks-spacing-2);
312+
z-index: 2;
313+
}
314+
}
315+
316+
.docs :deep(.plugin-doc) {
317+
max-width: 100%;
318+
background: transparent !important;
319+
}
320+
321+
.footer {
322+
display: flex;
323+
justify-content: flex-end;
324+
}
325+
</style>

0 commit comments

Comments
 (0)