@things-factory/scheduler 8.0.0-beta.9 → 8.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,484 @@
1
+ import '@material/web/icon/icon.js'
2
+ import '@material/web/textfield/filled-text-field.js'
3
+ import '@material/web/select/outlined-select.js'
4
+ import '@material/web/checkbox/checkbox.js'
5
+ import '@material/web/button/filled-button.js'
6
+ import '@operato/input/ox-input-data.js'
7
+ import '@operato/input/ox-input-key-values.js'
8
+
9
+ import { html, css, LitElement, PropertyValues } from 'lit'
10
+ import { customElement, property, query, state } from 'lit/decorators.js'
11
+ import gql from 'graphql-tag'
12
+
13
+ import { client } from '@operato/graphql'
14
+ import { i18next } from '@operato/i18n'
15
+ import { CommonHeaderStyles, ScrollbarStyles } from '@operato/styles'
16
+ import { notify, openPopup } from '@operato/layout'
17
+ import moment from '@operato/moment-timezone-es'
18
+
19
+ import { Schedule } from 'types'
20
+
21
+ function getSystemTimeZone(): string {
22
+ try {
23
+ const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
24
+ if (!timeZone) {
25
+ throw new Error('Unable to resolve timeZone')
26
+ }
27
+ return timeZone
28
+ } catch (e) {
29
+ console.warn('Failed to get system timeZone, falling back to UTC.', e)
30
+ return 'UTC'
31
+ }
32
+ }
33
+
34
+ const DEFAULT_TZ = getSystemTimeZone()
35
+ const TIMEZONE_OPTIONS = ['', DEFAULT_TZ, ...moment.tz.names().filter(tz => tz !== DEFAULT_TZ)]
36
+
37
+ @customElement('schedule-popup')
38
+ export class NewSchedulePopup extends LitElement {
39
+ static styles = [
40
+ ScrollbarStyles,
41
+ CommonHeaderStyles,
42
+ css`
43
+ :host {
44
+ display: flex;
45
+ flex-direction: column;
46
+ color: var(--popup-content-color);
47
+ background-color: var(--popup-content-background-color);
48
+ }
49
+
50
+ [content] {
51
+ flex: 1;
52
+ padding: var(--spacing-medium);
53
+ overflow: auto;
54
+ }
55
+
56
+ label {
57
+ display: flex;
58
+ flex-direction: row;
59
+ gap: 10px;
60
+ align-items: center;
61
+ }
62
+
63
+ md-filled-button,
64
+ md-outlined-button {
65
+ margin-top: 16px;
66
+ }
67
+
68
+ .footer {
69
+ display: flex;
70
+ align-items: center;
71
+ justify-content: space-between;
72
+ }
73
+ `
74
+ ]
75
+
76
+ @property({ type: Object }) closeCallback?: (schedule?: Schedule | null) => void
77
+ @property({ type: String, attribute: 'schedule-id' }) scheduleId?: string
78
+
79
+ @state() schedule: Schedule = {}
80
+
81
+ @query('form') form!: HTMLFormElement
82
+
83
+ render() {
84
+ const updatable = !!this.scheduleId
85
+
86
+ return html`
87
+ <div content>
88
+ <form @change=${this.onInputChanged}>
89
+ <md-filled-text-field
90
+ label="Name"
91
+ type="text"
92
+ id="name"
93
+ name="name"
94
+ .value=${this.schedule.name || ''}
95
+ required
96
+ ></md-filled-text-field>
97
+ <br /><br />
98
+
99
+ <fieldset>
100
+ <legend>Client Details</legend>
101
+ <md-filled-text-field
102
+ label="Application"
103
+ type="text"
104
+ id="application"
105
+ name="client.application"
106
+ .value=${this.schedule.client?.application || ''}
107
+ ?readonly=${updatable}
108
+ required
109
+ ></md-filled-text-field>
110
+ <br /><br />
111
+
112
+ <md-filled-text-field
113
+ label="Group"
114
+ type="text"
115
+ id="group"
116
+ name="client.group"
117
+ .value=${this.schedule.client?.group || ''}
118
+ ?readonly=${updatable}
119
+ required
120
+ ></md-filled-text-field>
121
+ <br /><br />
122
+
123
+ <md-filled-text-field
124
+ label="Type"
125
+ type="text"
126
+ id="type"
127
+ name="client.type"
128
+ .value=${this.schedule.client?.type || ''}
129
+ ?readonly=${updatable}
130
+ required
131
+ ></md-filled-text-field>
132
+ <br /><br />
133
+
134
+ <md-filled-text-field
135
+ label="Key"
136
+ type="text"
137
+ id="key"
138
+ name="client.key"
139
+ .value=${this.schedule.client?.key || ''}
140
+ ?readonly=${updatable}
141
+ required
142
+ ></md-filled-text-field>
143
+ <br /><br />
144
+
145
+ <md-filled-text-field
146
+ label="Operation"
147
+ type="text"
148
+ id="operation"
149
+ name="client.operation"
150
+ .value=${this.schedule.client?.operation || ''}
151
+ ?readonly=${updatable}
152
+ required
153
+ ></md-filled-text-field>
154
+ </fieldset>
155
+ <br />
156
+
157
+ <fieldset>
158
+ <legend>Schedule</legend>
159
+
160
+ <md-outlined-select label="Schedule Type" name="type" required>
161
+ <md-select-option value="now" ?selected=${this.schedule.type === 'now'}>Now</md-select-option>
162
+ <md-select-option value="delay" ?selected=${this.schedule.type === 'delay'}>Delay</md-select-option>
163
+ <md-select-option value="date" ?selected=${this.schedule.type === 'date'}>Date</md-select-option>
164
+ <md-select-option value="cron" ?selected=${this.schedule.type === 'cron'}>Cron</md-select-option>
165
+ <md-select-option value="delay-recur" ?selected=${this.schedule.type === 'delay-recur'}
166
+ >Delay-Recur</md-select-option
167
+ >
168
+ </md-outlined-select>
169
+ <br /><br />
170
+
171
+ <md-filled-text-field
172
+ label="Schedule (optional)"
173
+ type="text"
174
+ id="schedule"
175
+ name="schedule"
176
+ .value=${this.schedule.schedule || ''}
177
+ @click=${(e: MouseEvent) => {
178
+ const x = e.target as HTMLInputElement
179
+ openPopup(
180
+ html`
181
+ <ox-popup-crontab-input
182
+ .value=${x.value}
183
+ .confirmCallback=${value => {
184
+ x.value = value
185
+ x.dispatchEvent(new Event('change', { bubbles: true, cancelable: true }))
186
+ }}
187
+ ></ox-popup-crontab-input>
188
+ `,
189
+ {
190
+ backdrop: true,
191
+ title: i18next.t('title.setting schedule')
192
+ }
193
+ )
194
+ }}
195
+ ></md-filled-text-field>
196
+ <br /><br />
197
+
198
+ <md-outlined-select
199
+ label="Timezone (optional)"
200
+ id="timezone"
201
+ name="timezone"
202
+ .value=${this.schedule.timezone || ''}
203
+ required
204
+ >
205
+ ${TIMEZONE_OPTIONS.map(
206
+ tz =>
207
+ html` <md-select-option value=${tz} ?selected=${this.schedule.timezone === tz}
208
+ >${tz}</md-select-option
209
+ >`
210
+ )}
211
+ </md-outlined-select>
212
+ </fieldset>
213
+ <br /><br />
214
+
215
+ <fieldset>
216
+ <legend>Task Details</legend>
217
+ <md-outlined-select label="Task Type" name="task.type" required>
218
+ <md-select-option value="rest" ?selected=${this.schedule.task?.type === 'rest'}>REST</md-select-option>
219
+ <md-select-option value="kafka" ?selected=${this.schedule.task?.type === 'kafka'}>Kafka</md-select-option>
220
+ <md-select-option value="redis" ?selected=${this.schedule.task?.type === 'redis'}>Redis</md-select-option>
221
+ </md-outlined-select>
222
+ <br /><br />
223
+
224
+ <md-filled-text-field
225
+ label="Connection Host"
226
+ id="connectionHost"
227
+ name="task.connection.host"
228
+ .value=${this.schedule.task?.connection?.host || ''}
229
+ required
230
+ ></md-filled-text-field>
231
+ <br /><br />
232
+
233
+ <md-filled-text-field
234
+ label="Connection Topic (optional)"
235
+ id="connectionTopic"
236
+ name="task.connection.topic"
237
+ .value=${this.schedule.task?.connection?.topic || ''}
238
+ ></md-filled-text-field>
239
+ <br /><br />
240
+
241
+ <fieldset>
242
+ <legend>Connection Headers (optional)</legend>
243
+ <ox-input-key-values
244
+ name="task.connection.headers"
245
+ .value=${this.schedule.task?.connection?.headers || {}}
246
+ ></ox-input-key-values>
247
+ </fieldset>
248
+ <br /><br />
249
+
250
+ <fieldset>
251
+ <legend>Data (JSON)</legend>
252
+
253
+ <ox-input-data name="task.data" .value=${this.schedule.task?.data}> </ox-input-data>
254
+ </fieldset>
255
+ <br /><br />
256
+
257
+ <label>
258
+ <md-checkbox
259
+ type="checkbox"
260
+ id="historyCheck"
261
+ name="task.history_check"
262
+ .checked=${this.schedule.task?.history_check || false}
263
+ ></md-checkbox
264
+ >History Check
265
+ </label>
266
+ <br /><br />
267
+
268
+ <md-outlined-select label="Failed Policy" name="task.failed_policy" required>
269
+ <md-select-option value="ignore" ?selected=${this.schedule.task?.failed_policy === 'ignore'}
270
+ >Ignore</md-select-option
271
+ >
272
+ <md-select-option value="retry" ?selected=${this.schedule.task?.failed_policy === 'retry'}
273
+ >Retry</md-select-option
274
+ >
275
+ <md-select-option value="retry_dlq" ?selected=${this.schedule.task?.failed_policy === 'retry_dlq'}
276
+ >Retry DLQ</md-select-option
277
+ >
278
+ </md-outlined-select>
279
+ <br /><br />
280
+
281
+ <md-filled-text-field
282
+ label="Retry Wait (ms)"
283
+ type="number"
284
+ id="retryWait"
285
+ name="task.retry_wait"
286
+ .valueAsNumber=${this.schedule.task?.retry_wait || 0}
287
+ ></md-filled-text-field>
288
+ <br /><br />
289
+
290
+ <md-filled-text-field
291
+ label="Retry Count"
292
+ type="number"
293
+ id="retryCount"
294
+ name="task.retry_count"
295
+ .valueAsNumber=${this.schedule.task?.retry_count || 0}
296
+ ></md-filled-text-field>
297
+ <br /><br />
298
+
299
+ <md-filled-text-field
300
+ label="Max Retry Count"
301
+ type="number"
302
+ id="maxRetryCount"
303
+ name="task.max_retry_count"
304
+ .valueAsNumber=${this.schedule.task?.max_retry_count || 0}
305
+ ></md-filled-text-field>
306
+ </fieldset>
307
+ </form>
308
+ </div>
309
+
310
+ <div class="footer">
311
+ <button @click=${this.reset.bind(this)}><md-icon>restart_alt</md-icon>${i18next.t('button.reset')}</button>
312
+ <div filler></div>
313
+ <button @click=${this.cancel.bind(this)}><md-icon>cancel</md-icon>${i18next.t('button.cancel')}</button>
314
+ ${updatable
315
+ ? html` <button @click=${this.updateSchedule.bind(this)} done>
316
+ <md-icon>update</md-icon>${i18next.t('button.update')}
317
+ </button>`
318
+ : html` <button @click=${this.registerSchedule.bind(this)} done>
319
+ <md-icon>pending_actions</md-icon>${i18next.t('button.register')}
320
+ </button>`}
321
+ </div>
322
+ `
323
+ }
324
+
325
+ async updated(changes: PropertyValues<this>) {
326
+ if (changes.has('scheduleId')) {
327
+ this.schedule = this.scheduleId ? await this.fetchSchedule(this.scheduleId) : {}
328
+ }
329
+ }
330
+
331
+ private onInputChanged(event: Event) {
332
+ const target = event.target as HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
333
+ const nameParts = target.name.split('.')
334
+ let current = this.schedule
335
+
336
+ for (let i = 0; i < nameParts.length - 1; i++) {
337
+ const part = nameParts[i]
338
+ if (!current[part]) {
339
+ current[part] = {}
340
+ }
341
+ current = current[part]
342
+ }
343
+
344
+ const key = nameParts[nameParts.length - 1]
345
+ current[key] =
346
+ 'checked' in target
347
+ ? (target as HTMLInputElement).checked
348
+ : target.type === 'number'
349
+ ? Number(target.value)
350
+ : key == 'task.data'
351
+ ? JSON.parse(target.value)
352
+ : target.value
353
+
354
+ this.requestUpdate()
355
+ }
356
+
357
+ private reset() {
358
+ this.form.reset()
359
+ }
360
+
361
+ private cancel() {
362
+ history.back()
363
+ }
364
+
365
+ private prepareSchedule() {
366
+ this.schedule = {
367
+ ...this.schedule,
368
+ client: {
369
+ application: this.schedule.client?.application || '',
370
+ group: this.schedule.client?.group || '',
371
+ type: this.schedule.client?.type || '',
372
+ key: this.schedule.client?.key || '',
373
+ operation: this.schedule.client?.operation || ''
374
+ },
375
+ task: {
376
+ ...this.schedule.task,
377
+ data: this.schedule.task?.data || {},
378
+ history_check: this.schedule.task?.history_check ?? false,
379
+ failed_policy: this.schedule.task?.failed_policy || 'ignore',
380
+ retry_wait: this.schedule.task?.retry_wait || 0,
381
+ retry_count: this.schedule.task?.retry_count || 0,
382
+ max_retry_count: this.schedule.task?.max_retry_count || 0,
383
+ connection: {
384
+ ...this.schedule.task?.connection,
385
+ headers: this.schedule.task?.connection?.headers || {}
386
+ }
387
+ }
388
+ }
389
+ }
390
+
391
+ private async registerSchedule() {
392
+ this.prepareSchedule()
393
+
394
+ const { id, ...newSchedule } = this.schedule
395
+
396
+ const response = await client.mutate({
397
+ mutation: gql`
398
+ mutation ($schedule: NewSchedule!) {
399
+ handle: registerSchedule(schedule: $schedule)
400
+ }
401
+ `,
402
+ variables: {
403
+ schedule: newSchedule
404
+ }
405
+ })
406
+
407
+ if (response.data?.handle) {
408
+ this.scheduleId = response.data.handle
409
+
410
+ notify({
411
+ message: i18next.t('text.info_x_successfully', { x: i18next.t('text.register') })
412
+ })
413
+ }
414
+ }
415
+
416
+ private async updateSchedule() {
417
+ this.prepareSchedule()
418
+
419
+ const response = await client.mutate({
420
+ mutation: gql`
421
+ mutation UpdateSchedule($schedule: SchedulePatch!) {
422
+ handle: updateSchedule(schedule: $schedule)
423
+ }
424
+ `,
425
+ variables: {
426
+ schedule: this.schedule
427
+ }
428
+ })
429
+
430
+ if (response.data?.handle) {
431
+ this.scheduleId = response.data.handle
432
+
433
+ notify({
434
+ message: i18next.t('text.info_x_successfully', { x: i18next.t('text.update') })
435
+ })
436
+ }
437
+ }
438
+
439
+ private async fetchSchedule(id: string) {
440
+ const response = await client.query({
441
+ query: gql`
442
+ query ($id: ID!) {
443
+ schedule(id: $id) {
444
+ id
445
+ name
446
+ client {
447
+ application
448
+ group
449
+ type
450
+ key
451
+ operation
452
+ }
453
+ type
454
+ schedule
455
+ timezone
456
+ task {
457
+ type
458
+ connection {
459
+ host
460
+ headers
461
+ topic
462
+ }
463
+ data
464
+ history_check
465
+ failed_policy
466
+ retry_wait
467
+ retry_count
468
+ max_retry_count
469
+ }
470
+ }
471
+ }
472
+ `,
473
+ variables: {
474
+ id
475
+ }
476
+ })
477
+
478
+ if (response.data?.schedule) {
479
+ console.log(`a schedule fetched : ${response.data.schedule}`)
480
+
481
+ return response.data.schedule
482
+ }
483
+ }
484
+ }
@@ -0,0 +1,7 @@
1
+ export default function route(page: string) {
2
+ switch (page) {
3
+ case 'schedule-list':
4
+ import('./pages/scheduler/schedule-list-page')
5
+ return page
6
+ }
7
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "../../tsconfig-base.json",
3
+ "compilerOptions": {
4
+ "strict": true,
5
+ "declaration": true,
6
+ "module": "esnext",
7
+ "outDir": "../dist-client",
8
+ "baseUrl": "./"
9
+ },
10
+ "include": ["./**/*"]
11
+ }
@@ -0,0 +1,34 @@
1
+ export interface ScheduleClient {
2
+ application?: string
3
+ group?: string
4
+ type?: string
5
+ key?: string
6
+ operation?: string
7
+ }
8
+
9
+ export interface ScheduleTaskConnection {
10
+ host?: string
11
+ headers?: Record<string, string>
12
+ topic?: string
13
+ }
14
+
15
+ export interface ScheduleTask {
16
+ type?: string
17
+ connection?: ScheduleTaskConnection
18
+ data?: { [key: string]: any }
19
+ history_check?: boolean
20
+ failed_policy?: 'ignore' | 'retry' | 'retry_dlq'
21
+ retry_wait?: number
22
+ retry_count?: number
23
+ max_retry_count?: number
24
+ }
25
+
26
+ export interface Schedule {
27
+ id?: string
28
+ name?: string
29
+ client?: ScheduleClient
30
+ type?: 'now' | 'delay' | 'date' | 'cron' | 'delay-recur'
31
+ schedule?: string
32
+ timezone?: string
33
+ task?: ScheduleTask
34
+ }