import '@material/web/icon/icon.js' import '@operato/data-grist/ox-grist.js' import gql from 'graphql-tag' import { css, html, LitElement } from 'lit' import { customElement, property, query, state } from 'lit/decorators.js' import { DataGrist } from '@operato/data-grist/ox-grist.js' import { client } from '@operato/graphql' import { HelpDecoratedRenderer } from '@operato/help/help-decorated-renderer.js' import { i18next, localize } from '@operato/i18n' import { isMobileDevice } from '@operato/utils' import { CommonHeaderStyles } from '@operato/styles' import { FetchOption } from '@operato/data-grist' import { createEnvVarActionInjector, resolveEnvVars } from '../viewparts/env-var-action-injector.js' const SelectFields = ['name', 'description', 'sequence', 'task', 'connection', 'params', 'result', 'skip', 'log'] @customElement('scenario-detail') class ScenarioDetail extends localize(i18next)(LitElement) { static styles = [ CommonHeaderStyles, css` :host { display: flex; flex-direction: column; background-color: var(--md-sys-color-surface); } ox-grist { flex: 1; } ` ] @property({ type: Object }) scenario: any @property({ type: Object }) gristConfig: any @property({ type: Object }) taskTypes: any @query('ox-grist') grist!: DataGrist render() { return html` ` } async firstUpdated() { this.gristConfig = { list: { fields: ['name', 'description', 'task'] }, columns: [ { type: 'gutter', gutterName: 'row-selector', multiple: true, fixed: true }, { type: 'gutter', gutterName: 'button', fixed: true, icon: 'add', handlers: { click: 'record-copy' } }, { type: 'gutter', gutterName: 'sequence', fixed: true }, { type: 'gutter', gutterName: 'button', fixed: true, icon: 'arrow_upward', handlers: { click: 'move-up' } }, { type: 'gutter', gutterName: 'button', fixed: true, icon: 'arrow_downward', handlers: { click: 'move-down' } }, { type: 'number', name: 'sequence', fixed: true, hidden: true }, { type: 'string', name: 'id', hidden: true }, { type: 'string', name: 'name', fixed: true, header: i18next.t('field.name'), record: { editable: true }, width: 140 }, { type: 'string', name: 'description', header: i18next.t('field.description'), record: { editable: true }, width: 240 }, { type: 'boolean', name: 'result', header: i18next.t('field.result'), record: { editable: true }, width: 80 }, { type: 'boolean', name: 'skip', header: i18next.t('field.skip'), record: { editable: true }, width: 80 }, { type: 'boolean', name: 'log', header: i18next.t('field.log'), record: { editable: true }, width: 80 }, { type: 'connection', name: 'connection', header: i18next.t('field.connection'), record: { editable: true }, width: 160 }, { type: 'task-type', name: 'task', header: i18next.t('field.task'), record: { renderer: HelpDecoratedRenderer, editable: true, help: value => this.taskTypes?.[value]?.help, connectionName: async (value, column, record, row, field) => { /* return connectionName */ return record.connection } }, width: 200 }, { type: 'parameters', name: 'params', header: i18next.t('field.params'), record: { editable: true, options: async (value, column, record, row, field) => { // 등록된 task type 이 아닌 step.task 디스트럭처 보호 + 진단 경고 let taskEntry: any = null if (record.task) { taskEntry = this.taskTypes?.[record.task] || null if (!taskEntry && this.taskTypes) { console.warn( `[scenario-detail] task type '${record.task}' (step '${record.name}') is not registered. ` + `Server-side handler missing or not bootstrapped.` ) } } var { name, parameterSpec: spec, help } = taskEntry || ({} as any) const context = this.grist const scenarioName = this.scenario?.name const stepName = record?.name const domainAttrParams = (Array.isArray(spec) ? spec : []).filter( (p: any) => p?.useDomainAttribute && p?.name ) const keys = scenarioName && stepName ? domainAttrParams.map((p: any) => `Step::${scenarioName}::${stepName}::${p.name}`) : [] const resolutions = await resolveEnvVars(keys) return { name, spec, help, context, objectified: true, actionInjector: scenarioName && stepName ? createEnvVarActionInjector( (propName: string) => `Step::${scenarioName}::${stepName}::${propName}`, resolutions, () => this.grist.fetch() ) : undefined } }, renderer: 'json5' }, width: 300 } ], rows: { selectable: { multiple: true } }, pagination: { infinite: true }, sorters: [ { name: 'sequence' } ] } } async fetchHandler({ page, limit, sorters = [] }: FetchOption) { const response = await client.query({ query: gql` query { steps ( filters: [{ name: "scenarioId", value: "${this.scenario.id}", operator: "eq" }], sortings: { name: "sequence" } ) { items { id ${SelectFields.join('\n')} } total } } ` }) return { total: response.data.steps.total || 0, records: response.data.steps.items || [] } } async fetchTaskType(name) { const response = await client.query({ query: gql` query ($name: String!) { taskType(name: $name) { name description help parameterSpec { type name label placeholder property styles } } } `, variables: { name } }) return response.data.taskType } async _updateSteps() { let patches = this.grist.dirtyData.records if (patches) { patches = patches.map(patch => { const { __origin__: { __typename, ...patchField } = {}, __dirtyfields__ } = patch for (let key in __dirtyfields__) { patchField[key] = __dirtyfields__[key].after } return patchField }) const response = await client.mutate({ mutation: gql` mutation ($scenarioId: String!, $patches: [StepPatch!]!) { updateMultipleStep(scenarioId: $scenarioId, patches: $patches) { name } } `, variables: { scenarioId: this.scenario.id, patches } }) if (!response.errors) { this.grist.fetch() this.requestRefresh() } } } async _deleteSteps() { this.grist.deleteSelectedRecords(true) } requestRefresh() { this.dispatchEvent(new CustomEvent('requestRefresh')) } }