@things-factory/integration-ui 10.0.0-beta.92 → 10.0.0-beta.95
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 +347 -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 +54 -0
- package/dist-client/viewparts/env-var-quick-editor.js +351 -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
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import '@material/web/button/filled-button.js'
|
|
2
|
+
import '@material/web/button/outlined-button.js'
|
|
3
|
+
import '@material/web/button/text-button.js'
|
|
4
|
+
import '@material/web/textfield/outlined-text-field.js'
|
|
5
|
+
import '@material/web/icon/icon.js'
|
|
6
|
+
|
|
7
|
+
import gql from 'graphql-tag'
|
|
8
|
+
import { LitElement, css, html } from 'lit'
|
|
9
|
+
import { customElement, property, state } from 'lit/decorators.js'
|
|
10
|
+
import { client } from '@operato/graphql'
|
|
11
|
+
import { notify } from '@operato/layout'
|
|
12
|
+
import { i18next, localize } from '@operato/i18n'
|
|
13
|
+
|
|
14
|
+
interface Resolution {
|
|
15
|
+
key: string
|
|
16
|
+
status: 'local' | 'inherited' | 'absent'
|
|
17
|
+
envVarId?: string
|
|
18
|
+
sourceDomainName?: string
|
|
19
|
+
value?: string
|
|
20
|
+
hasValue?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Connection / Step 파라미터 화면에서 useDomainAttribute 속성의 EnvVar 를
|
|
25
|
+
* 한 자리에서 확인·편집·삭제하는 인라인 편집기.
|
|
26
|
+
*
|
|
27
|
+
* 동작:
|
|
28
|
+
* - 현 도메인에 값이 있으면 update(M) / delete
|
|
29
|
+
* - 상속만 있는 경우(또는 없는 경우) "이 도메인에 등록" (create)
|
|
30
|
+
* - 부모 값을 덮어쓰면 closest-wins 로 자식 값이 적용됨을 안내
|
|
31
|
+
*
|
|
32
|
+
* 저장/삭제 성공 시 `saved` 이벤트 발생.
|
|
33
|
+
*/
|
|
34
|
+
@customElement('env-var-quick-editor')
|
|
35
|
+
export class EnvVarQuickEditor extends localize(i18next)(LitElement) {
|
|
36
|
+
static styles = css`
|
|
37
|
+
:host {
|
|
38
|
+
display: flex;
|
|
39
|
+
flex-direction: column;
|
|
40
|
+
min-width: 460px;
|
|
41
|
+
max-height: 80vh;
|
|
42
|
+
padding: 20px 24px;
|
|
43
|
+
box-sizing: border-box;
|
|
44
|
+
background: var(--md-sys-color-surface, #fff);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
h3 {
|
|
48
|
+
margin: 0 0 4px;
|
|
49
|
+
font-size: 16px;
|
|
50
|
+
flex: 0 0 auto;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* 스크롤 가능 본문 — flex column 안에서 actions 위쪽 영역만 흘러내림 */
|
|
54
|
+
.body {
|
|
55
|
+
flex: 1 1 auto;
|
|
56
|
+
overflow: auto;
|
|
57
|
+
min-height: 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.key {
|
|
61
|
+
font-family: var(--md-sys-typescale-body-medium-font-family, monospace);
|
|
62
|
+
font-size: 12px;
|
|
63
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
64
|
+
background: var(--md-sys-color-surface-variant);
|
|
65
|
+
padding: 4px 8px;
|
|
66
|
+
border-radius: 6px;
|
|
67
|
+
display: inline-block;
|
|
68
|
+
margin-bottom: 16px;
|
|
69
|
+
word-break: break-all;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.status {
|
|
73
|
+
display: flex;
|
|
74
|
+
align-items: center;
|
|
75
|
+
gap: 8px;
|
|
76
|
+
margin-bottom: 14px;
|
|
77
|
+
font-size: 14px;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.status[data-status='local'] {
|
|
81
|
+
color: var(--md-sys-color-tertiary);
|
|
82
|
+
}
|
|
83
|
+
.status[data-status='inherited'] {
|
|
84
|
+
color: var(--md-sys-color-primary);
|
|
85
|
+
}
|
|
86
|
+
.status[data-status='absent'] {
|
|
87
|
+
color: var(--md-sys-color-error);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
md-outlined-text-field {
|
|
91
|
+
width: 100%;
|
|
92
|
+
margin-bottom: 12px;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.actions {
|
|
96
|
+
display: flex;
|
|
97
|
+
justify-content: flex-end;
|
|
98
|
+
gap: 8px;
|
|
99
|
+
flex: 0 0 auto;
|
|
100
|
+
padding-top: 12px;
|
|
101
|
+
margin-top: 4px;
|
|
102
|
+
border-top: 1px solid var(--md-sys-color-outline-variant, #e0e0e0);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.info {
|
|
106
|
+
font-size: 12px;
|
|
107
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
108
|
+
margin-bottom: 12px;
|
|
109
|
+
line-height: 1.5;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.scope {
|
|
113
|
+
font-size: 12px;
|
|
114
|
+
line-height: 1.6;
|
|
115
|
+
padding: 10px 12px;
|
|
116
|
+
margin-bottom: 14px;
|
|
117
|
+
border-radius: 8px;
|
|
118
|
+
background: var(--md-sys-color-surface-container-low, #f5f5f5);
|
|
119
|
+
border-left: 3px solid var(--md-sys-color-primary, #3457d5);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.scope .scope-title {
|
|
123
|
+
font-weight: 600;
|
|
124
|
+
color: var(--md-sys-color-on-surface);
|
|
125
|
+
margin-bottom: 4px;
|
|
126
|
+
display: flex;
|
|
127
|
+
align-items: center;
|
|
128
|
+
gap: 4px;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.scope .scope-title md-icon {
|
|
132
|
+
font-size: 14px;
|
|
133
|
+
--md-icon-size: 14px;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.scope ul {
|
|
137
|
+
margin: 4px 0 0;
|
|
138
|
+
padding-left: 18px;
|
|
139
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.scope li {
|
|
143
|
+
margin-bottom: 2px;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.scope code {
|
|
147
|
+
font-family: var(--md-sys-typescale-body-medium-font-family, monospace);
|
|
148
|
+
font-size: 11px;
|
|
149
|
+
background: var(--md-sys-color-surface-variant);
|
|
150
|
+
padding: 1px 4px;
|
|
151
|
+
border-radius: 3px;
|
|
152
|
+
}
|
|
153
|
+
`
|
|
154
|
+
|
|
155
|
+
/** EnvVar 키 (예: `Connection::kiscon-conn::password`) */
|
|
156
|
+
@property({ type: String }) key_!: string
|
|
157
|
+
|
|
158
|
+
/** 속성 사양 (type/secret 여부 표시 등에 사용) */
|
|
159
|
+
@property({ type: Object }) propSpec: any
|
|
160
|
+
|
|
161
|
+
/** 사전 조회한 해소 상태 */
|
|
162
|
+
@property({ type: Object }) resolution!: Resolution
|
|
163
|
+
|
|
164
|
+
/** 표시용 라벨 (속성 이름) */
|
|
165
|
+
@property({ type: String }) propLabel: string = ''
|
|
166
|
+
|
|
167
|
+
@state() private editedValue: string = ''
|
|
168
|
+
@state() private busy: boolean = false
|
|
169
|
+
|
|
170
|
+
connectedCallback() {
|
|
171
|
+
super.connectedCallback()
|
|
172
|
+
this.editedValue = this.resolution?.value ?? ''
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private get isSecret(): boolean {
|
|
176
|
+
return this.propSpec?.type === 'secret' || /password|secret|token|key$/i.test(this.propLabel || '')
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 저장 시 영향 범위 (scope) 안내 패널 — 운영자 의사결정 보조.
|
|
181
|
+
* - absent : 처음 등록. 자손 트리 전체에 inherit.
|
|
182
|
+
* - local : 이미 등록됨. 자손이 inherit (자손에 자기 override 있으면 그것 우선).
|
|
183
|
+
* - inherited: 상위에서 받고있음. 여기 등록 시 이 도메인 + 자손에만 한정 override.
|
|
184
|
+
*/
|
|
185
|
+
private _renderScopePanel(isLocal: boolean, isInherited: boolean) {
|
|
186
|
+
if (isLocal) {
|
|
187
|
+
return html`
|
|
188
|
+
<div class="scope">
|
|
189
|
+
<div class="scope-title">
|
|
190
|
+
<md-icon>info</md-icon>
|
|
191
|
+
저장 위치 / 영향 범위
|
|
192
|
+
</div>
|
|
193
|
+
이 도메인에 이미 등록된 값이 자손 도메인 전체에 inherit 됩니다 (closest-wins).
|
|
194
|
+
<ul>
|
|
195
|
+
<li>여기서 값을 바꾸면 → 이 도메인 + 자손 트리 전체에 즉시 반영</li>
|
|
196
|
+
<li>특정 자손에 자기 override 가 등록되어 있으면 그쪽은 자기 값 우선</li>
|
|
197
|
+
<li>"이 도메인에서 삭제" → 부모(있다면) 값으로 fallback</li>
|
|
198
|
+
</ul>
|
|
199
|
+
</div>
|
|
200
|
+
`
|
|
201
|
+
}
|
|
202
|
+
if (isInherited) {
|
|
203
|
+
return html`
|
|
204
|
+
<div class="scope">
|
|
205
|
+
<div class="scope-title">
|
|
206
|
+
<md-icon>inventory</md-icon>
|
|
207
|
+
저장 위치 / 영향 범위
|
|
208
|
+
</div>
|
|
209
|
+
현재 <code>${this.resolution.sourceDomainName || '상위 도메인'}</code> 에서 상속된 값을 사용 중입니다.
|
|
210
|
+
<ul>
|
|
211
|
+
<li>여기서 등록하면 → 이 도메인 + 그 자손에만 우선 적용 (다른 형제 도메인은 영향 없음)</li>
|
|
212
|
+
<li>나머지 다른 자손들은 여전히
|
|
213
|
+
<code>${this.resolution.sourceDomainName || '상위 도메인'}</code> 의 값을 그대로 사용</li>
|
|
214
|
+
</ul>
|
|
215
|
+
</div>
|
|
216
|
+
`
|
|
217
|
+
}
|
|
218
|
+
return html`
|
|
219
|
+
<div class="scope">
|
|
220
|
+
<div class="scope-title">
|
|
221
|
+
<md-icon>add_circle</md-icon>
|
|
222
|
+
저장 위치 / 영향 범위
|
|
223
|
+
</div>
|
|
224
|
+
이 도메인에 처음 등록합니다.
|
|
225
|
+
<ul>
|
|
226
|
+
<li>저장 후 → 이 도메인 + 자손 트리 전체에서 사용</li>
|
|
227
|
+
<li>특정 자손이 자기 값을 별도 등록하면 그쪽은 자기 값 우선 (closest-wins)</li>
|
|
228
|
+
<li>잘못된 도메인에 저장하지 않도록 — 현재 컨텍스트 도메인을 한번 확인하세요</li>
|
|
229
|
+
</ul>
|
|
230
|
+
</div>
|
|
231
|
+
`
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private get statusLabel(): string {
|
|
235
|
+
const s = this.resolution?.status
|
|
236
|
+
if (s === 'local') return `✓ ${i18next.t('text.set-on-this-domain') || '이 도메인에 등록됨'}`
|
|
237
|
+
if (s === 'inherited')
|
|
238
|
+
return `🔗 ${i18next.t('text.inherited-from') || '상속'} (${this.resolution.sourceDomainName || '부모 도메인'})`
|
|
239
|
+
return `⚠ ${i18next.t('text.not-set') || '미설정'}`
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
render() {
|
|
243
|
+
const isLocal = this.resolution?.status === 'local'
|
|
244
|
+
const isInherited = this.resolution?.status === 'inherited'
|
|
245
|
+
|
|
246
|
+
return html`
|
|
247
|
+
<h3>${i18next.t('text.domain-attribute') || '도메인 속성'} · ${this.propLabel || this.key_}</h3>
|
|
248
|
+
|
|
249
|
+
<div class="body">
|
|
250
|
+
<div class="key">${this.key_}</div>
|
|
251
|
+
|
|
252
|
+
<div class="status" data-status=${this.resolution?.status || 'absent'}>${this.statusLabel}</div>
|
|
253
|
+
|
|
254
|
+
${this._renderScopePanel(isLocal, isInherited)}
|
|
255
|
+
|
|
256
|
+
<md-outlined-text-field
|
|
257
|
+
label=${i18next.t('field.value')}
|
|
258
|
+
type=${this.isSecret ? 'password' : 'text'}
|
|
259
|
+
.value=${this.editedValue}
|
|
260
|
+
@input=${(e: any) => (this.editedValue = e.target.value)}
|
|
261
|
+
?disabled=${this.busy}
|
|
262
|
+
></md-outlined-text-field>
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
<div class="actions">
|
|
266
|
+
${isLocal
|
|
267
|
+
? html`<md-text-button @click=${this._delete} ?disabled=${this.busy}>
|
|
268
|
+
${i18next.t('button.delete-from-this-domain') || '이 도메인에서 삭제'}
|
|
269
|
+
</md-text-button>`
|
|
270
|
+
: ''}
|
|
271
|
+
<md-outlined-button @click=${this._cancel} ?disabled=${this.busy}>
|
|
272
|
+
${i18next.t('button.cancel')}
|
|
273
|
+
</md-outlined-button>
|
|
274
|
+
<md-filled-button @click=${this._save} ?disabled=${this.busy || !this.editedValue}>
|
|
275
|
+
${isLocal ? i18next.t('button.update') || '갱신' : i18next.t('button.set') || '등록'}
|
|
276
|
+
</md-filled-button>
|
|
277
|
+
</div>
|
|
278
|
+
`
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private _cancel() {
|
|
282
|
+
this.dispatchEvent(new CustomEvent('cancel', { bubbles: true, composed: true }))
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private async _save() {
|
|
286
|
+
this.busy = true
|
|
287
|
+
try {
|
|
288
|
+
const isUpdate = this.resolution?.status === 'local' && this.resolution?.envVarId
|
|
289
|
+
const patches = isUpdate
|
|
290
|
+
? [{ id: this.resolution.envVarId, cuFlag: 'M', value: this.editedValue, active: true }]
|
|
291
|
+
: [
|
|
292
|
+
{
|
|
293
|
+
cuFlag: '+',
|
|
294
|
+
name: this.key_,
|
|
295
|
+
value: this.editedValue,
|
|
296
|
+
active: true,
|
|
297
|
+
description: this.propLabel || ''
|
|
298
|
+
}
|
|
299
|
+
]
|
|
300
|
+
|
|
301
|
+
const response = await client.mutate({
|
|
302
|
+
mutation: gql`
|
|
303
|
+
mutation ($patches: [EnvVarPatch!]!) {
|
|
304
|
+
updateMultipleEnvVars(patches: $patches) {
|
|
305
|
+
name
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
`,
|
|
309
|
+
variables: { patches }
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
if (response.errors) throw new Error(response.errors.map((e: any) => e.message).join('\n'))
|
|
313
|
+
|
|
314
|
+
notify({ message: i18next.t('text.saved-successfully') || '저장되었습니다' })
|
|
315
|
+
this.dispatchEvent(new CustomEvent('saved', { bubbles: true, composed: true }))
|
|
316
|
+
} catch (e: any) {
|
|
317
|
+
notify({ level: 'error', message: e.message || String(e) })
|
|
318
|
+
} finally {
|
|
319
|
+
this.busy = false
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private async _delete() {
|
|
324
|
+
if (!this.resolution?.envVarId) return
|
|
325
|
+
// 도메인 환경변수 삭제는 확인 없이 즉시 처리.
|
|
326
|
+
this.busy = true
|
|
327
|
+
try {
|
|
328
|
+
const response = await client.mutate({
|
|
329
|
+
mutation: gql`
|
|
330
|
+
mutation ($id: String!) {
|
|
331
|
+
deleteEnvVar(id: $id)
|
|
332
|
+
}
|
|
333
|
+
`,
|
|
334
|
+
variables: { id: this.resolution.envVarId }
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
if (response.errors) throw new Error(response.errors.map((e: any) => e.message).join('\n'))
|
|
338
|
+
|
|
339
|
+
notify({ message: i18next.t('text.deleted-successfully') || '삭제되었습니다' })
|
|
340
|
+
this.dispatchEvent(new CustomEvent('saved', { bubbles: true, composed: true }))
|
|
341
|
+
} catch (e: any) {
|
|
342
|
+
notify({ level: 'error', message: e.message || String(e) })
|
|
343
|
+
} finally {
|
|
344
|
+
this.busy = false
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
@@ -45,8 +45,14 @@ export declare class Connection extends Connection_base {
|
|
|
45
45
|
pageInitialized(): Promise<void>;
|
|
46
46
|
fetchHandler({ page, limit, sortings, filters }: FetchOption): Promise<{
|
|
47
47
|
total: any;
|
|
48
|
-
records: any;
|
|
48
|
+
records: any[];
|
|
49
49
|
}>;
|
|
50
|
+
/**
|
|
51
|
+
* 한 페이지 분 connections 에 대해 useDomainAttribute params 키를 모두 모아 단일
|
|
52
|
+
* envVarResolutions 호출로 해소한다. 그 후 각 connection 의 키 부분집합에 대해
|
|
53
|
+
* diagnoseReadiness 로 _readiness 를 주입.
|
|
54
|
+
*/
|
|
55
|
+
_annotateReadiness(items: any[]): Promise<any[]>;
|
|
50
56
|
fetchConnectors(): Promise<void>;
|
|
51
57
|
_deleteConnections(name: any): Promise<void>;
|
|
52
58
|
_updateConnectionManager(): Promise<void>;
|
|
@@ -13,44 +13,7 @@ import { PageView } from '@operato/shell';
|
|
|
13
13
|
import { CommonButtonStyles, CommonGristStyles, ScrollbarStyles } from '@operato/styles';
|
|
14
14
|
import { isMobileDevice } from '@operato/utils';
|
|
15
15
|
import { p13n } from '@operato/p13n';
|
|
16
|
-
|
|
17
|
-
try {
|
|
18
|
-
await navigator.clipboard.writeText(text);
|
|
19
|
-
}
|
|
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
|
-
function createActionInjector(connectionName) {
|
|
30
|
-
return (propName, propSpec) => {
|
|
31
|
-
if (!propSpec.useDomainAttribute) {
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
const attributeName = `Connection::${connectionName}::${propName}`;
|
|
35
|
-
const copyIcon = document.createElement('md-icon');
|
|
36
|
-
copyIcon.textContent = 'dictionary';
|
|
37
|
-
copyIcon.style.cssText =
|
|
38
|
-
'cursor: pointer; color: var(--md-sys-color-primary); font-size: 16px; --md-icon-size: 16px;';
|
|
39
|
-
copyIcon.title = `Copy ${attributeName}`;
|
|
40
|
-
copyIcon.addEventListener('click', () => {
|
|
41
|
-
copyToClipboard(attributeName);
|
|
42
|
-
// 복사 성공 피드백
|
|
43
|
-
const originalText = copyIcon.textContent;
|
|
44
|
-
copyIcon.textContent = 'check';
|
|
45
|
-
copyIcon.style.color = 'var(--md-sys-color-tertiary)';
|
|
46
|
-
setTimeout(() => {
|
|
47
|
-
copyIcon.textContent = originalText;
|
|
48
|
-
copyIcon.style.color = 'var(--md-sys-color-primary)';
|
|
49
|
-
}, 1000);
|
|
50
|
-
});
|
|
51
|
-
return copyIcon;
|
|
52
|
-
};
|
|
53
|
-
}
|
|
16
|
+
import { createEnvVarActionInjector, diagnoseReadiness, resolveEnvVars } from '../viewparts/env-var-action-injector.js';
|
|
54
17
|
let Connection = class Connection extends p13n(localize(i18next)(PageView)) {
|
|
55
18
|
constructor() {
|
|
56
19
|
super(...arguments);
|
|
@@ -196,6 +159,27 @@ let Connection = class Connection extends p13n(localize(i18next)(PageView)) {
|
|
|
196
159
|
},
|
|
197
160
|
width: 120
|
|
198
161
|
},
|
|
162
|
+
{
|
|
163
|
+
type: 'select',
|
|
164
|
+
name: 'inheritanceMode',
|
|
165
|
+
label: true,
|
|
166
|
+
header: i18next.t('field.inheritance-mode') || '상속 모드',
|
|
167
|
+
record: {
|
|
168
|
+
editable: true,
|
|
169
|
+
// GraphQL enum 이름 (대문자) 을 value 로 사용 — type-graphql registerEnumType 이
|
|
170
|
+
// 노출하는 형식과 일치해야 함. 서버 내부 runtime 값('isolate'/'share') 으로의
|
|
171
|
+
// 변환은 GraphQL 레이어가 자동.
|
|
172
|
+
options: [
|
|
173
|
+
{ display: '(connector 기본)', value: null },
|
|
174
|
+
{ display: 'ISOLATE — 자식별 격리 인스턴스 (기본·안전)', value: 'ISOLATE' },
|
|
175
|
+
{ display: 'SHARE — 자식이 부모 인스턴스 공유', value: 'SHARE' }
|
|
176
|
+
]
|
|
177
|
+
},
|
|
178
|
+
// SHARE 의 의미는 select 옵션 라벨에 명시 ("자식이 부모 인스턴스 공유").
|
|
179
|
+
// 운영자가 의식적으로 선택. native confirm() 차단 다이얼로그 제거 — 잘못 선택해도
|
|
180
|
+
// 즉시 ISOLATE / (connector 기본) 으로 되돌릴 수 있음.
|
|
181
|
+
width: 200
|
|
182
|
+
},
|
|
199
183
|
{ type: 'connector',
|
|
200
184
|
name: 'type',
|
|
201
185
|
label: true,
|
|
@@ -224,18 +208,46 @@ let Connection = class Connection extends p13n(localize(i18next)(PageView)) {
|
|
|
224
208
|
header: i18next.t('field.params'),
|
|
225
209
|
record: { editable: true,
|
|
226
210
|
options: async (value, column, record, row, field) => {
|
|
227
|
-
|
|
211
|
+
// 동시에 운영자가 데이터 측 문제를 인지할 수 있도록 콘솔 경고를 남김.
|
|
212
|
+
let connectorEntry = null;
|
|
213
|
+
if (record.type) {
|
|
214
|
+
connectorEntry = this.connectors?.[record.type] || null;
|
|
215
|
+
if (!connectorEntry && this.connectors) {
|
|
216
|
+
console.warn(`[connection] connector type '${record.type}' (connection '${record.name}') is not registered. ` +
|
|
217
|
+
`Server-side package missing or not bootstrapped.`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const { name, help, parameterSpec: spec } = connectorEntry || {};
|
|
228
221
|
const context = this.grist;
|
|
222
|
+
// useDomainAttribute params 의 EnvVar 해소 상태 사전 조회
|
|
223
|
+
const domainAttrParams = (Array.isArray(spec) ? spec : []).filter((p) => p?.useDomainAttribute && p?.name);
|
|
224
|
+
const keys = domainAttrParams.map((p) => `Connection::${record.name}::${p.name}`);
|
|
225
|
+
const resolutions = await resolveEnvVars(keys);
|
|
229
226
|
return { name,
|
|
230
227
|
help,
|
|
231
228
|
spec,
|
|
232
229
|
context,
|
|
233
230
|
objectified: true,
|
|
234
|
-
actionInjector:
|
|
231
|
+
actionInjector: createEnvVarActionInjector((propName) => `Connection::${record.name}::${propName}`, resolutions, () => this.grist.fetch())
|
|
235
232
|
};
|
|
236
233
|
},
|
|
237
234
|
renderer: 'json5' },
|
|
238
235
|
width: 100 },
|
|
236
|
+
{
|
|
237
|
+
type: 'string',
|
|
238
|
+
name: '_readiness',
|
|
239
|
+
header: i18next.t('field.domain-attribute-readiness') || '준비 상태',
|
|
240
|
+
record: {
|
|
241
|
+
editable: false,
|
|
242
|
+
renderer: (_v, _c, r) => {
|
|
243
|
+
const d = r?._readiness;
|
|
244
|
+
if (!d)
|
|
245
|
+
return '';
|
|
246
|
+
return d.label;
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
width: 180
|
|
250
|
+
},
|
|
239
251
|
{ type: 'resource-object',
|
|
240
252
|
name: 'edge',
|
|
241
253
|
header: i18next.t('field.edge-server'),
|
|
@@ -287,6 +299,7 @@ let Connection = class Connection extends p13n(localize(i18next)(PageView)) {
|
|
|
287
299
|
endpoint
|
|
288
300
|
active
|
|
289
301
|
onDemand
|
|
302
|
+
inheritanceMode
|
|
290
303
|
state
|
|
291
304
|
params
|
|
292
305
|
updater { id
|
|
@@ -304,10 +317,40 @@ let Connection = class Connection extends p13n(localize(i18next)(PageView)) {
|
|
|
304
317
|
sortings
|
|
305
318
|
}
|
|
306
319
|
});
|
|
320
|
+
const items = response.data.responses.items || [];
|
|
321
|
+
const records = await this._annotateReadiness(items);
|
|
307
322
|
return { total: response.data.responses.total || 0,
|
|
308
|
-
records
|
|
323
|
+
records
|
|
309
324
|
};
|
|
310
325
|
}
|
|
326
|
+
/**
|
|
327
|
+
* 한 페이지 분 connections 에 대해 useDomainAttribute params 키를 모두 모아 단일
|
|
328
|
+
* envVarResolutions 호출로 해소한다. 그 후 각 connection 의 키 부분집합에 대해
|
|
329
|
+
* diagnoseReadiness 로 _readiness 를 주입.
|
|
330
|
+
*/
|
|
331
|
+
async _annotateReadiness(items) {
|
|
332
|
+
if (!items || items.length === 0)
|
|
333
|
+
return items;
|
|
334
|
+
if (!this.connectors) { // connectors 로딩 전 호출되었으면 일단 그대로
|
|
335
|
+
return items;
|
|
336
|
+
}
|
|
337
|
+
const perConn = [];
|
|
338
|
+
const allKeys = [];
|
|
339
|
+
for (const conn of items) {
|
|
340
|
+
const connector = this.connectors[conn.type];
|
|
341
|
+
const spec = Array.isArray(connector?.parameterSpec) ? connector.parameterSpec : [];
|
|
342
|
+
const keys = spec
|
|
343
|
+
.filter((p) => p?.useDomainAttribute && p?.name)
|
|
344
|
+
.map((p) => `Connection::${conn.name}::${p.name}`);
|
|
345
|
+
perConn.push({ conn, keys });
|
|
346
|
+
allKeys.push(...keys);
|
|
347
|
+
}
|
|
348
|
+
const uniqueKeys = Array.from(new Set(allKeys));
|
|
349
|
+
const resolutions = await resolveEnvVars(uniqueKeys);
|
|
350
|
+
return perConn.map(({ conn, keys }) => ({ ...conn,
|
|
351
|
+
_readiness: diagnoseReadiness(resolutions, keys)
|
|
352
|
+
}));
|
|
353
|
+
}
|
|
311
354
|
async fetchConnectors() {
|
|
312
355
|
const response = await client.query({ query: gql `
|
|
313
356
|
query { connectors { items { name
|
|
@@ -330,6 +373,10 @@ let Connection = class Connection extends p13n(localize(i18next)(PageView)) {
|
|
|
330
373
|
connectors[connector.name] = connector;
|
|
331
374
|
return connectors;
|
|
332
375
|
}, {});
|
|
376
|
+
// connectors 가 준비된 시점에 readiness 가 비어있다면 한 번 더 fetch
|
|
377
|
+
if (this.grist?.dirtyData?.records?.some?.((r) => !r._readiness)) {
|
|
378
|
+
this.grist.fetch();
|
|
379
|
+
}
|
|
333
380
|
}
|
|
334
381
|
else {
|
|
335
382
|
console.error('fetch connectors error');
|