import '@material/web/button/filled-button.js' import '@material/web/button/outlined-button.js' import '@material/web/button/text-button.js' import '@material/web/icon/icon.js' import gql from 'graphql-tag' import { LitElement, css, html } from 'lit' import { customElement, property, state } from 'lit/decorators.js' import { client } from '@operato/graphql' import { notify } from '@operato/layout' import { OxPropertyEditor } from '@operato/property-editor' import { i18next, localize } from '@operato/i18n' interface Resolution { key: string status: 'local' | 'inherited' | 'absent' envVarId?: string sourceDomainName?: string value?: string hasValue?: boolean } /** * Connection / Step 파라미터 화면에서 useDomainAttribute 속성의 EnvVar 를 * 한 자리에서 확인·편집·삭제하는 인라인 편집기. * * 동작: * - 현 도메인에 값이 있으면 update(M) / delete * - 상속만 있는 경우(또는 없는 경우) "이 도메인에 등록" (create) * - 부모 값을 덮어쓰면 closest-wins 로 자식 값이 적용됨을 안내 * * 저장/삭제 성공 시 `saved` 이벤트 발생. */ @customElement('env-var-quick-editor') export class EnvVarQuickEditor extends localize(i18next)(LitElement) { static styles = css` :host { display: flex; flex-direction: column; min-width: 460px; max-height: 80vh; padding: 20px 24px; box-sizing: border-box; background: var(--md-sys-color-surface, #fff); } h3 { margin: 0 0 4px; font-size: 16px; flex: 0 0 auto; } /* 스크롤 가능 본문 — flex column 안에서 actions 위쪽 영역만 흘러내림 */ .body { flex: 1 1 auto; overflow: auto; min-height: 0; } .key { font-family: var(--md-sys-typescale-body-medium-font-family, monospace); font-size: 12px; color: var(--md-sys-color-on-surface-variant); background: var(--md-sys-color-surface-variant); padding: 4px 8px; border-radius: 6px; display: inline-block; margin-bottom: 16px; word-break: break-all; } .status { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; font-size: 14px; } .status[data-status='local'] { color: var(--md-sys-color-tertiary); } .status[data-status='inherited'] { color: var(--md-sys-color-primary); } .status[data-status='absent'] { color: var(--md-sys-color-error); } md-outlined-text-field { width: 100%; margin-bottom: 12px; } .actions { display: flex; justify-content: flex-end; gap: 8px; flex: 0 0 auto; padding-top: 12px; margin-top: 4px; border-top: 1px solid var(--md-sys-color-outline-variant, #e0e0e0); } .info { font-size: 12px; color: var(--md-sys-color-on-surface-variant); margin-bottom: 12px; line-height: 1.5; } .scope { font-size: 12px; line-height: 1.6; padding: 10px 12px; margin-bottom: 14px; border-radius: 8px; background: var(--md-sys-color-surface-container-low, #f5f5f5); border-left: 3px solid var(--md-sys-color-primary, #3457d5); } .scope .scope-title { font-weight: 600; color: var(--md-sys-color-on-surface); margin-bottom: 4px; display: flex; align-items: center; gap: 4px; } .scope .scope-title md-icon { font-size: 14px; --md-icon-size: 14px; } .scope ul { margin: 4px 0 0; padding-left: 18px; color: var(--md-sys-color-on-surface-variant); } .scope li { margin-bottom: 2px; } .scope code { font-family: var(--md-sys-typescale-body-medium-font-family, monospace); font-size: 11px; background: var(--md-sys-color-surface-variant); padding: 1px 4px; border-radius: 3px; } ` /** EnvVar 키 (예: `Connection::kiscon-conn::password`) */ @property({ type: String }) key_!: string /** 속성 사양 (type/secret 여부 표시 등에 사용) */ @property({ type: Object }) propSpec: any /** 사전 조회한 해소 상태 */ @property({ type: Object }) resolution!: Resolution /** 표시용 라벨 (속성 이름) */ @property({ type: String }) propLabel: string = '' @state() private editedValue: string = '' @state() private busy: boolean = false connectedCallback() { super.connectedCallback() this.editedValue = this.resolution?.value ?? '' } private get isSecret(): boolean { return this.propSpec?.type === 'secret' || /password|secret|token|key$/i.test(this.propLabel || '') } /** * 저장 시 영향 범위 (scope) 안내 패널 — 운영자 의사결정 보조. * - absent : 처음 등록. 자손 트리 전체에 inherit. * - local : 이미 등록됨. 자손이 inherit (자손에 자기 override 있으면 그것 우선). * - inherited: 상위에서 받고있음. 여기 등록 시 이 도메인 + 자손에만 한정 override. */ private _renderScopePanel(isLocal: boolean, isInherited: boolean) { if (isLocal) { return html`
${this.resolution.sourceDomainName || '상위 도메인'} 에서 상속된 값을 사용 중입니다.
${this.resolution.sourceDomainName || '상위 도메인'} 의 값을 그대로 사용