@things-factory/integration-ui 10.0.0-beta.92 → 10.0.0-beta.96
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.
- package/client/pages/connection.ts +101 -43
- package/client/pages/scenario-detail.ts +41 -3
- package/client/pages/scenario.ts +1 -0
- package/client/viewparts/env-var-action-injector.ts +239 -0
- package/client/viewparts/env-var-quick-editor.ts +436 -0
- package/dist-client/pages/connection.d.ts +7 -1
- package/dist-client/pages/connection.js +88 -41
- package/dist-client/pages/connection.js.map +1 -1
- package/dist-client/pages/scenario-detail.js +29 -3
- package/dist-client/pages/scenario-detail.js.map +1 -1
- package/dist-client/pages/scenario.js +1 -0
- package/dist-client/pages/scenario.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/viewparts/env-var-action-injector.d.ts +38 -0
- package/dist-client/viewparts/env-var-action-injector.js +196 -0
- package/dist-client/viewparts/env-var-action-injector.js.map +1 -0
- package/dist-client/viewparts/env-var-quick-editor.d.ts +68 -0
- package/dist-client/viewparts/env-var-quick-editor.js +435 -0
- package/dist-client/viewparts/env-var-quick-editor.js.map +1 -0
- package/package.json +3 -3
- package/translations/en.json +18 -6
- package/translations/ja.json +18 -6
- package/translations/ko.json +18 -6
- package/translations/ms.json +18 -6
- package/translations/zh.json +18 -6
|
@@ -14,47 +14,12 @@ import { CommonButtonStyles, CommonGristStyles, ScrollbarStyles } from '@operato
|
|
|
14
14
|
import { isMobileDevice } from '@operato/utils'
|
|
15
15
|
import { FetchOption } from '@operato/data-grist'
|
|
16
16
|
import { p13n } from '@operato/p13n'
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
createEnvVarActionInjector,
|
|
19
|
+
diagnoseReadiness,
|
|
20
|
+
resolveEnvVars
|
|
21
|
+
} from '../viewparts/env-var-action-injector.js'
|
|
18
22
|
|
|
19
|
-
async function copyToClipboard(text: string): Promise<void> { try { await navigator.clipboard.writeText(text)
|
|
20
|
-
} catch (err) { // fallback: old way
|
|
21
|
-
const textArea = document.createElement('textarea')
|
|
22
|
-
textArea.value = text
|
|
23
|
-
document.body.appendChild(textArea)
|
|
24
|
-
textArea.select()
|
|
25
|
-
document.execCommand('copy')
|
|
26
|
-
document.body.removeChild(textArea)
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function createActionInjector(
|
|
31
|
-
connectionName: string
|
|
32
|
-
): (propName: string, propSpec: PropertySpec) => HTMLElement | null { return (propName: string, propSpec: PropertySpec): HTMLElement | null => { if (!propSpec.useDomainAttribute) { return null
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const attributeName = `Connection::${connectionName}::${propName}`
|
|
36
|
-
|
|
37
|
-
const copyIcon = document.createElement('md-icon')
|
|
38
|
-
copyIcon.textContent = 'dictionary'
|
|
39
|
-
copyIcon.style.cssText =
|
|
40
|
-
'cursor: pointer; color: var(--md-sys-color-primary); font-size: 16px; --md-icon-size: 16px;'
|
|
41
|
-
copyIcon.title = `Copy ${attributeName}`
|
|
42
|
-
|
|
43
|
-
copyIcon.addEventListener('click', () => { copyToClipboard(attributeName)
|
|
44
|
-
|
|
45
|
-
// 복사 성공 피드백
|
|
46
|
-
const originalText = copyIcon.textContent
|
|
47
|
-
copyIcon.textContent = 'check'
|
|
48
|
-
copyIcon.style.color = 'var(--md-sys-color-tertiary)'
|
|
49
|
-
|
|
50
|
-
setTimeout(() => { copyIcon.textContent = originalText
|
|
51
|
-
copyIcon.style.color = 'var(--md-sys-color-primary)'
|
|
52
|
-
}, 1000)
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
return copyIcon
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
23
|
|
|
59
24
|
@customElement('connection-page')
|
|
60
25
|
export class Connection extends p13n(localize(i18next)(PageView)) { static styles = [
|
|
@@ -197,6 +162,27 @@ export class Connection extends p13n(localize(i18next)(PageView)) { static sty
|
|
|
197
162
|
record: { editable: true
|
|
198
163
|
},
|
|
199
164
|
width: 120
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
type: 'select',
|
|
168
|
+
name: 'inheritanceMode',
|
|
169
|
+
label: true,
|
|
170
|
+
header: i18next.t('field.inheritance-mode') || '상속 모드',
|
|
171
|
+
record: {
|
|
172
|
+
editable: true,
|
|
173
|
+
// GraphQL enum 이름 (대문자) 을 value 로 사용 — type-graphql registerEnumType 이
|
|
174
|
+
// 노출하는 형식과 일치해야 함. 서버 내부 runtime 값('isolate'/'share') 으로의
|
|
175
|
+
// 변환은 GraphQL 레이어가 자동.
|
|
176
|
+
options: [
|
|
177
|
+
{ display: '(connector 기본)', value: null },
|
|
178
|
+
{ display: 'ISOLATE — 자식별 격리 인스턴스 (기본·안전)', value: 'ISOLATE' },
|
|
179
|
+
{ display: 'SHARE — 자식이 부모 인스턴스 공유', value: 'SHARE' }
|
|
180
|
+
]
|
|
181
|
+
},
|
|
182
|
+
// SHARE 의 의미는 select 옵션 라벨에 명시 ("자식이 부모 인스턴스 공유").
|
|
183
|
+
// 운영자가 의식적으로 선택. native confirm() 차단 다이얼로그 제거 — 잘못 선택해도
|
|
184
|
+
// 즉시 ISOLATE / (connector 기본) 으로 되돌릴 수 있음.
|
|
185
|
+
width: 200
|
|
200
186
|
},
|
|
201
187
|
{ type: 'connector',
|
|
202
188
|
name: 'type',
|
|
@@ -225,20 +211,53 @@ export class Connection extends p13n(localize(i18next)(PageView)) { static sty
|
|
|
225
211
|
name: 'params',
|
|
226
212
|
header: i18next.t('field.params'),
|
|
227
213
|
record: { editable: true,
|
|
228
|
-
options: async (value, column, record, row, field) => {
|
|
214
|
+
options: async (value, column, record, row, field) => { // 등록된 connector 가 아닌 type 을 가진 Connection 에서 디스트럭처 죽음을 막고,
|
|
215
|
+
// 동시에 운영자가 데이터 측 문제를 인지할 수 있도록 콘솔 경고를 남김.
|
|
216
|
+
let connectorEntry: any = null
|
|
217
|
+
if (record.type) { connectorEntry = this.connectors?.[record.type] || null
|
|
218
|
+
if (!connectorEntry && this.connectors) { console.warn(
|
|
219
|
+
`[connection] connector type '${record.type}' (connection '${record.name}') is not registered. ` +
|
|
220
|
+
`Server-side package missing or not bootstrapped.`
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const { name, help, parameterSpec: spec } = connectorEntry || ({} as any)
|
|
229
225
|
const context = this.grist
|
|
230
226
|
|
|
227
|
+
// useDomainAttribute params 의 EnvVar 해소 상태 사전 조회
|
|
228
|
+
const domainAttrParams = (Array.isArray(spec) ? spec : []).filter((p: any) => p?.useDomainAttribute && p?.name)
|
|
229
|
+
const keys = domainAttrParams.map((p: any) => `Connection::${record.name}::${p.name}`)
|
|
230
|
+
const resolutions = await resolveEnvVars(keys)
|
|
231
|
+
|
|
231
232
|
return { name,
|
|
232
233
|
help,
|
|
233
234
|
spec,
|
|
234
235
|
context,
|
|
235
236
|
objectified: true,
|
|
236
|
-
actionInjector:
|
|
237
|
+
actionInjector: createEnvVarActionInjector(
|
|
238
|
+
(propName: string) => `Connection::${record.name}::${propName}`,
|
|
239
|
+
resolutions,
|
|
240
|
+
() => this.grist.fetch()
|
|
241
|
+
)
|
|
237
242
|
}
|
|
238
243
|
},
|
|
239
244
|
renderer: 'json5'
|
|
240
245
|
},
|
|
241
246
|
width: 100
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
type: 'string',
|
|
250
|
+
name: '_readiness',
|
|
251
|
+
header: i18next.t('field.domain-attribute-readiness') || '준비 상태',
|
|
252
|
+
record: {
|
|
253
|
+
editable: false,
|
|
254
|
+
renderer: (_v: any, _c: any, r: any) => {
|
|
255
|
+
const d = r?._readiness
|
|
256
|
+
if (!d) return ''
|
|
257
|
+
return d.label
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
width: 180
|
|
242
261
|
},
|
|
243
262
|
{ type: 'resource-object',
|
|
244
263
|
name: 'edge',
|
|
@@ -292,6 +311,7 @@ export class Connection extends p13n(localize(i18next)(PageView)) { static sty
|
|
|
292
311
|
endpoint
|
|
293
312
|
active
|
|
294
313
|
onDemand
|
|
314
|
+
inheritanceMode
|
|
295
315
|
state
|
|
296
316
|
params
|
|
297
317
|
updater { id
|
|
@@ -310,9 +330,43 @@ export class Connection extends p13n(localize(i18next)(PageView)) { static sty
|
|
|
310
330
|
}
|
|
311
331
|
})
|
|
312
332
|
|
|
333
|
+
const items = response.data.responses.items || []
|
|
334
|
+
const records = await this._annotateReadiness(items)
|
|
335
|
+
|
|
313
336
|
return { total: response.data.responses.total || 0,
|
|
314
|
-
records
|
|
337
|
+
records
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* 한 페이지 분 connections 에 대해 useDomainAttribute params 키를 모두 모아 단일
|
|
343
|
+
* envVarResolutions 호출로 해소한다. 그 후 각 connection 의 키 부분집합에 대해
|
|
344
|
+
* diagnoseReadiness 로 _readiness 를 주입.
|
|
345
|
+
*/
|
|
346
|
+
async _annotateReadiness(items: any[]): Promise<any[]> { if (!items || items.length === 0) return items
|
|
347
|
+
if (!this.connectors) { // connectors 로딩 전 호출되었으면 일단 그대로
|
|
348
|
+
return items
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
type ConnKeys = { conn: any; keys: string[] }
|
|
352
|
+
const perConn: ConnKeys[] = []
|
|
353
|
+
const allKeys: string[] = []
|
|
354
|
+
|
|
355
|
+
for (const conn of items) { const connector = this.connectors[conn.type]
|
|
356
|
+
const spec = Array.isArray(connector?.parameterSpec) ? connector.parameterSpec : []
|
|
357
|
+
const keys = spec
|
|
358
|
+
.filter((p: any) => p?.useDomainAttribute && p?.name)
|
|
359
|
+
.map((p: any) => `Connection::${conn.name}::${p.name}`)
|
|
360
|
+
perConn.push({ conn, keys })
|
|
361
|
+
allKeys.push(...keys)
|
|
315
362
|
}
|
|
363
|
+
|
|
364
|
+
const uniqueKeys = Array.from(new Set(allKeys))
|
|
365
|
+
const resolutions = await resolveEnvVars(uniqueKeys)
|
|
366
|
+
|
|
367
|
+
return perConn.map(({ conn, keys }) => ({ ...conn,
|
|
368
|
+
_readiness: diagnoseReadiness(resolutions, keys)
|
|
369
|
+
}))
|
|
316
370
|
}
|
|
317
371
|
|
|
318
372
|
async fetchConnectors() { const response = await client.query({ query: gql`
|
|
@@ -335,6 +389,10 @@ export class Connection extends p13n(localize(i18next)(PageView)) { static sty
|
|
|
335
389
|
if (!response.errors) { this.connectors = response.data.connectors.items.reduce((connectors, connector) => { connectors[connector.name] = connector
|
|
336
390
|
return connectors
|
|
337
391
|
}, {})
|
|
392
|
+
|
|
393
|
+
// connectors 가 준비된 시점에 readiness 가 비어있다면 한 번 더 fetch
|
|
394
|
+
if (this.grist?.dirtyData?.records?.some?.((r: any) => !r._readiness)) { this.grist.fetch()
|
|
395
|
+
}
|
|
338
396
|
} else { console.error('fetch connectors error')
|
|
339
397
|
}
|
|
340
398
|
}
|
|
@@ -13,6 +13,8 @@ import { isMobileDevice } from '@operato/utils'
|
|
|
13
13
|
import { CommonHeaderStyles } from '@operato/styles'
|
|
14
14
|
import { FetchOption } from '@operato/data-grist'
|
|
15
15
|
|
|
16
|
+
import { createEnvVarActionInjector, resolveEnvVars } from '../viewparts/env-var-action-injector.js'
|
|
17
|
+
|
|
16
18
|
const SelectFields = ['name', 'description', 'sequence', 'task', 'connection', 'params', 'result', 'skip', 'log']
|
|
17
19
|
|
|
18
20
|
@customElement('scenario-detail')
|
|
@@ -176,10 +178,46 @@ class ScenarioDetail extends localize(i18next)(LitElement) {
|
|
|
176
178
|
header: i18next.t('field.params'),
|
|
177
179
|
record: {
|
|
178
180
|
editable: true,
|
|
179
|
-
options: (value, column, record, row, field) => {
|
|
180
|
-
|
|
181
|
+
options: async (value, column, record, row, field) => {
|
|
182
|
+
// 등록된 task type 이 아닌 step.task 디스트럭처 보호 + 진단 경고
|
|
183
|
+
let taskEntry: any = null
|
|
184
|
+
if (record.task) {
|
|
185
|
+
taskEntry = this.taskTypes?.[record.task] || null
|
|
186
|
+
if (!taskEntry && this.taskTypes) {
|
|
187
|
+
console.warn(
|
|
188
|
+
`[scenario-detail] task type '${record.task}' (step '${record.name}') is not registered. ` +
|
|
189
|
+
`Server-side handler missing or not bootstrapped.`
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
var { name, parameterSpec: spec, help } = taskEntry || ({} as any)
|
|
181
194
|
const context = this.grist
|
|
182
|
-
|
|
195
|
+
|
|
196
|
+
const scenarioName = this.scenario?.name
|
|
197
|
+
const stepName = record?.name
|
|
198
|
+
const domainAttrParams = (Array.isArray(spec) ? spec : []).filter(
|
|
199
|
+
(p: any) => p?.useDomainAttribute && p?.name
|
|
200
|
+
)
|
|
201
|
+
const keys = scenarioName && stepName
|
|
202
|
+
? domainAttrParams.map((p: any) => `Step::${scenarioName}::${stepName}::${p.name}`)
|
|
203
|
+
: []
|
|
204
|
+
const resolutions = await resolveEnvVars(keys)
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
name,
|
|
208
|
+
spec,
|
|
209
|
+
help,
|
|
210
|
+
context,
|
|
211
|
+
objectified: true,
|
|
212
|
+
actionInjector:
|
|
213
|
+
scenarioName && stepName
|
|
214
|
+
? createEnvVarActionInjector(
|
|
215
|
+
(propName: string) => `Step::${scenarioName}::${stepName}::${propName}`,
|
|
216
|
+
resolutions,
|
|
217
|
+
() => this.grist.fetch()
|
|
218
|
+
)
|
|
219
|
+
: undefined
|
|
220
|
+
}
|
|
183
221
|
},
|
|
184
222
|
renderer: 'json5'
|
|
185
223
|
},
|
package/client/pages/scenario.ts
CHANGED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import '@material/web/icon/icon.js'
|
|
2
|
+
import './env-var-quick-editor.js'
|
|
3
|
+
|
|
4
|
+
import gql from 'graphql-tag'
|
|
5
|
+
import { html } from 'lit'
|
|
6
|
+
import { client } from '@operato/graphql'
|
|
7
|
+
import { notify, openPopup } from '@operato/layout'
|
|
8
|
+
import { i18next } from '@operato/i18n'
|
|
9
|
+
import { PropertySpec } from '../types.js'
|
|
10
|
+
|
|
11
|
+
export interface EnvVarResolution {
|
|
12
|
+
key: string
|
|
13
|
+
status: 'local' | 'inherited' | 'absent'
|
|
14
|
+
envVarId?: string
|
|
15
|
+
sourceDomainId?: string
|
|
16
|
+
sourceDomainName?: string
|
|
17
|
+
value?: string
|
|
18
|
+
hasValue?: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* useDomainAttribute 속성의 EnvVar 4-상태 칩 + 인라인 편집기 팝업.
|
|
23
|
+
* Connection / Step params 화면이 공통으로 사용.
|
|
24
|
+
*/
|
|
25
|
+
export async function resolveEnvVars(keys: string[]): Promise<Map<string, EnvVarResolution>> {
|
|
26
|
+
const map = new Map<string, EnvVarResolution>()
|
|
27
|
+
if (!keys || keys.length === 0) return map
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const response = await client.query({
|
|
31
|
+
query: gql`
|
|
32
|
+
query ($keys: [String!]!) {
|
|
33
|
+
envVarResolutions(keys: $keys) {
|
|
34
|
+
key
|
|
35
|
+
status
|
|
36
|
+
envVarId
|
|
37
|
+
sourceDomainId
|
|
38
|
+
sourceDomainName
|
|
39
|
+
value
|
|
40
|
+
hasValue
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
`,
|
|
44
|
+
variables: { keys },
|
|
45
|
+
fetchPolicy: 'network-only'
|
|
46
|
+
})
|
|
47
|
+
for (const r of response.data?.envVarResolutions || []) {
|
|
48
|
+
map.set(r.key, r)
|
|
49
|
+
}
|
|
50
|
+
} catch (e) {
|
|
51
|
+
console.warn('envVarResolutions failed; falling back to absent', e)
|
|
52
|
+
}
|
|
53
|
+
return map
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function copyToClipboard(text: string): Promise<void> {
|
|
57
|
+
try {
|
|
58
|
+
await navigator.clipboard.writeText(text)
|
|
59
|
+
} catch {
|
|
60
|
+
const textArea = document.createElement('textarea')
|
|
61
|
+
textArea.value = text
|
|
62
|
+
document.body.appendChild(textArea)
|
|
63
|
+
textArea.select()
|
|
64
|
+
document.execCommand('copy')
|
|
65
|
+
document.body.removeChild(textArea)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function buildStatusChip(
|
|
70
|
+
key: string,
|
|
71
|
+
resolution: EnvVarResolution | undefined,
|
|
72
|
+
onClick: () => void,
|
|
73
|
+
onCopy: () => void
|
|
74
|
+
): HTMLElement {
|
|
75
|
+
const chip = document.createElement('span')
|
|
76
|
+
chip.style.cssText = [
|
|
77
|
+
'display:inline-flex',
|
|
78
|
+
'align-items:center',
|
|
79
|
+
'gap:4px',
|
|
80
|
+
'padding:2px 8px',
|
|
81
|
+
'font-size:11px',
|
|
82
|
+
'line-height:1.4',
|
|
83
|
+
'border-radius:10px',
|
|
84
|
+
'cursor:pointer',
|
|
85
|
+
'user-select:none',
|
|
86
|
+
'border:1px solid'
|
|
87
|
+
].join(';')
|
|
88
|
+
|
|
89
|
+
const status = resolution?.status || 'absent'
|
|
90
|
+
let icon = 'help'
|
|
91
|
+
let label = '미설정'
|
|
92
|
+
let fg = ''
|
|
93
|
+
let bg = ''
|
|
94
|
+
|
|
95
|
+
if (status === 'local') {
|
|
96
|
+
icon = 'check_circle'
|
|
97
|
+
label = '이 도메인'
|
|
98
|
+
fg = 'var(--md-sys-color-on-tertiary-container)'
|
|
99
|
+
bg = 'var(--md-sys-color-tertiary-container)'
|
|
100
|
+
} else if (status === 'inherited') {
|
|
101
|
+
icon = 'inventory'
|
|
102
|
+
label = `상속${resolution!.sourceDomainName ? ' · ' + resolution!.sourceDomainName : ''}`
|
|
103
|
+
fg = 'var(--md-sys-color-on-primary-container)'
|
|
104
|
+
bg = 'var(--md-sys-color-primary-container)'
|
|
105
|
+
} else {
|
|
106
|
+
icon = 'warning'
|
|
107
|
+
label = '미설정'
|
|
108
|
+
fg = 'var(--md-sys-color-on-error-container)'
|
|
109
|
+
bg = 'var(--md-sys-color-error-container)'
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
chip.style.color = fg
|
|
113
|
+
chip.style.background = bg
|
|
114
|
+
chip.style.borderColor = fg
|
|
115
|
+
chip.title = `${key} — 클릭: 편집, ⌥ 클릭: 키 복사`
|
|
116
|
+
|
|
117
|
+
const iconEl = document.createElement('md-icon')
|
|
118
|
+
iconEl.textContent = icon
|
|
119
|
+
iconEl.style.cssText = `font-size:13px; --md-icon-size:13px; color:${fg};`
|
|
120
|
+
chip.appendChild(iconEl)
|
|
121
|
+
|
|
122
|
+
const text = document.createElement('span')
|
|
123
|
+
text.textContent = label
|
|
124
|
+
chip.appendChild(text)
|
|
125
|
+
|
|
126
|
+
chip.addEventListener('click', (e: MouseEvent) => {
|
|
127
|
+
if (e.altKey) onCopy()
|
|
128
|
+
else onClick()
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
return chip
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function openEnvVarEditor(
|
|
135
|
+
key: string,
|
|
136
|
+
propName: string,
|
|
137
|
+
propSpec: PropertySpec,
|
|
138
|
+
resolution: EnvVarResolution,
|
|
139
|
+
refresh: () => void
|
|
140
|
+
): void {
|
|
141
|
+
const propLabel = (propSpec.label && i18next.t('label.' + propSpec.label)) || propName
|
|
142
|
+
|
|
143
|
+
const popup = openPopup(
|
|
144
|
+
html`
|
|
145
|
+
<env-var-quick-editor
|
|
146
|
+
.key_=${key}
|
|
147
|
+
.propSpec=${propSpec}
|
|
148
|
+
.resolution=${resolution}
|
|
149
|
+
.propLabel=${propLabel}
|
|
150
|
+
@saved=${() => {
|
|
151
|
+
popup.close?.()
|
|
152
|
+
refresh()
|
|
153
|
+
}}
|
|
154
|
+
@cancel=${() => popup.close?.()}
|
|
155
|
+
></env-var-quick-editor>
|
|
156
|
+
`,
|
|
157
|
+
{
|
|
158
|
+
backdrop: true,
|
|
159
|
+
size: 'small',
|
|
160
|
+
title: `${propLabel} — ${key}`
|
|
161
|
+
}
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* actionInjector 팩토리.
|
|
167
|
+
*
|
|
168
|
+
* @param keyBuilder 속성 이름을 받아 EnvVar 키를 생성. 예:
|
|
169
|
+
* Connection 화면: `(p) => 'Connection::' + connName + '::' + p`
|
|
170
|
+
* Step 화면: `(p) => 'Step::' + scenarioName + '::' + stepName + '::' + p`
|
|
171
|
+
* @param resolutions 사전 조회한 해소 상태 맵 (key → resolution)
|
|
172
|
+
* @param refresh 저장·삭제 후 부모 grist 새로고침 콜백
|
|
173
|
+
*/
|
|
174
|
+
export function createEnvVarActionInjector(
|
|
175
|
+
keyBuilder: (propName: string) => string,
|
|
176
|
+
resolutions: Map<string, EnvVarResolution>,
|
|
177
|
+
refresh: () => void
|
|
178
|
+
): (propName: string, propSpec: PropertySpec) => HTMLElement | null {
|
|
179
|
+
return (propName, propSpec) => {
|
|
180
|
+
if (!propSpec.useDomainAttribute) return null
|
|
181
|
+
const key = keyBuilder(propName)
|
|
182
|
+
let currentResolution = resolutions.get(key) || { key, status: 'absent' as const }
|
|
183
|
+
|
|
184
|
+
// 안정 컨테이너 — DOM 상의 위치/참조는 유지하고 내부 chip 만 교체.
|
|
185
|
+
// chip 직접 replaceWith 시 shadow DOM 경계나 grist 재렌더에 의해
|
|
186
|
+
// 칩이 사라지는 케이스가 있어 컨테이너로 한 단계 감쌈.
|
|
187
|
+
const host = document.createElement('span')
|
|
188
|
+
host.style.cssText = 'display:inline-flex; align-items:center;'
|
|
189
|
+
|
|
190
|
+
const openCopy = () => copyToClipboard(key).then(() => notify({ message: `복사됨: ${key}` }))
|
|
191
|
+
|
|
192
|
+
const renderChip = () => {
|
|
193
|
+
const chip = buildStatusChip(key, currentResolution, openEdit, openCopy)
|
|
194
|
+
host.replaceChildren(chip)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 저장·삭제 후 자기 chip 만 즉시 갱신. 부모 grist 의 fetch 는 호출하지 않는다 —
|
|
198
|
+
// grist.fetch() 가 parameters 편집기(popover) 자체를 닫아 칩들이 통째로 사라지는
|
|
199
|
+
// 사고를 막기 위함. 다음 cell edit 저장 시점에 자연스럽게 grid 가 새로고침됨.
|
|
200
|
+
const onSaved = async () => {
|
|
201
|
+
try {
|
|
202
|
+
const updated = await resolveEnvVars([key])
|
|
203
|
+
currentResolution = updated.get(key) || { key, status: 'absent' as const }
|
|
204
|
+
renderChip()
|
|
205
|
+
} catch (e) {
|
|
206
|
+
console.warn('chip 갱신 실패', e)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const openEdit = () => openEnvVarEditor(key, propName, propSpec, currentResolution, onSaved)
|
|
211
|
+
|
|
212
|
+
renderChip()
|
|
213
|
+
return host
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* 한 화면(Connection 한 행, Scenario 한 step 셋) 의 진단 라벨.
|
|
219
|
+
* - 사용 준비됨 (n/n) : 모든 useDomainAttribute 속성이 local 또는 inherited
|
|
220
|
+
* - 미완성 (m absent / n total): 하나라도 absent
|
|
221
|
+
*/
|
|
222
|
+
export function diagnoseReadiness(
|
|
223
|
+
resolutions: Map<string, EnvVarResolution>,
|
|
224
|
+
expectedKeys: string[]
|
|
225
|
+
): { ready: boolean; total: number; absent: number; label: string } {
|
|
226
|
+
const total = expectedKeys.length
|
|
227
|
+
let absent = 0
|
|
228
|
+
for (const k of expectedKeys) {
|
|
229
|
+
const r = resolutions.get(k)
|
|
230
|
+
if (!r || r.status === 'absent') absent++
|
|
231
|
+
}
|
|
232
|
+
const ready = absent === 0 && total > 0
|
|
233
|
+
const label = total === 0
|
|
234
|
+
? '도메인 속성 없음'
|
|
235
|
+
: ready
|
|
236
|
+
? `사용 준비됨 (${total}/${total})`
|
|
237
|
+
: `미완성 — ${absent} 미설정 / ${total}`
|
|
238
|
+
return { ready, total, absent, label }
|
|
239
|
+
}
|