@things-factory/integration-ui 10.0.0-zeta.8 → 10.0.0

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.
@@ -1,6 +1,8 @@
1
+ import '@material/web/icon/icon.js'
1
2
  import '@operato/data-grist'
2
3
  import './scenario-detail.js'
3
4
  import './scenario-importer.js'
5
+ import '../viewparts/scenario-versions.js'
4
6
 
5
7
  import gql from 'graphql-tag'
6
8
  import { css, html } from 'lit'
@@ -9,6 +11,7 @@ import { DataGrist } from '@operato/data-grist/ox-grist.js'
9
11
  import { client } from '@operato/graphql'
10
12
  import { i18next, localize } from '@operato/i18n'
11
13
  import { notify, openPopup } from '@operato/layout'
14
+ import { OxPrompt } from '@operato/popup/ox-prompt.js'
12
15
  import { navigate, PageView } from '@operato/shell'
13
16
  import { CommonButtonStyles, CommonGristStyles, ScrollbarStyles } from '@operato/styles'
14
17
  import { isMobileDevice } from '@operato/utils'
@@ -249,6 +252,69 @@ export class Scenario extends p13n(localize(i18next)(PageView)) { static get s
249
252
  sortable: true,
250
253
  width: 60
251
254
  },
255
+ {
256
+ type: 'string',
257
+ name: 'publishState',
258
+ header: i18next.t('field.publish state'),
259
+ record: {
260
+ editable: false,
261
+ align: 'center',
262
+ // released → 라벨, draft → 'release' 버튼(클릭 시 릴리즈). 별도 버튼 컬럼 없이 상태 컬럼에서 처리.
263
+ renderer: (value, column, record) => {
264
+ if (!record || !record.id) return html``
265
+ if (value === 'released') {
266
+ return html`
267
+ <span
268
+ style="display:inline-flex;align-items:center;gap:3px;color:var(--md-sys-color-primary);font-size:11px;opacity:0.85;"
269
+ >
270
+ <md-icon style="font-size:15px;">check_circle</md-icon>${i18next.t('label.released')}
271
+ </span>
272
+ `
273
+ }
274
+ return html`
275
+ <button
276
+ style="cursor:pointer;display:inline-flex;align-items:center;gap:3px;font:inherit;font-size:11px;color:var(--md-sys-color-on-primary);background:var(--md-sys-color-primary);border:none;border-radius:999px;padding:2px 11px;"
277
+ title=${i18next.t('button.release')}
278
+ @click=${(e: Event) => {
279
+ e.stopPropagation()
280
+ this.releaseScenario(record)
281
+ }}
282
+ >
283
+ <md-icon style="font-size:15px;">publish</md-icon>${i18next.t('button.release')}
284
+ </button>
285
+ `
286
+ }
287
+ },
288
+ width: 100
289
+ },
290
+ {
291
+ type: 'number',
292
+ name: 'version',
293
+ header: i18next.t('field.version'),
294
+ record: {
295
+ editable: false,
296
+ align: 'center',
297
+ // 버전 번호 옆에 history 아이콘 — 클릭 시 버전 이력 열기(별도 컬럼 없이 version 컬럼 재사용).
298
+ renderer: (value, column, record) => {
299
+ if (!record || !record.id) return html``
300
+ return html`
301
+ <span style="display:inline-flex;align-items:center;justify-content:center;gap:4px;">
302
+ v${value ?? 0}
303
+ <md-icon
304
+ style="cursor:pointer;font-size:17px;color:var(--md-sys-color-primary);"
305
+ title=${i18next.t('button.version history')}
306
+ @click=${(e: Event) => {
307
+ e.stopPropagation()
308
+ this.openVersionHistory(record)
309
+ }}
310
+ >history</md-icon
311
+ >
312
+ </span>
313
+ `
314
+ }
315
+ },
316
+ width: 82
317
+ },
252
318
  { type: 'object',
253
319
  name: 'updater',
254
320
  header: i18next.t('field.updater'),
@@ -286,6 +352,8 @@ export class Scenario extends p13n(localize(i18next)(PageView)) { static get s
286
352
  description
287
353
  active
288
354
  state
355
+ publishState
356
+ version
289
357
  iteration
290
358
  schedule
291
359
  scheduleId
@@ -316,7 +384,8 @@ export class Scenario extends p13n(localize(i18next)(PageView)) { static get s
316
384
  variables: { filters,
317
385
  pagination: { page, limit },
318
386
  sortings
319
- }
387
+ },
388
+ fetchPolicy: 'no-cache'
320
389
  })
321
390
 
322
391
  return { total: response.data.responses.total || 0,
@@ -406,13 +475,66 @@ export class Scenario extends p13n(localize(i18next)(PageView)) { static get s
406
475
  }
407
476
  }
408
477
 
478
+ async releaseScenario(record) {
479
+ // 릴리즈 코멘트(선택) — OxPrompt.prompt(모달, native prompt 대체). 비우면 서버가 diff 요약으로 자동 채움.
480
+ const input = await OxPrompt.prompt({
481
+ type: 'question',
482
+ title: String(i18next.t('button.release')),
483
+ text: String(i18next.t('text.release comment optional')),
484
+ multiline: true,
485
+ confirmButton: { text: String(i18next.t('button.release')) },
486
+ cancelButton: { text: String(i18next.t('button.cancel')) }
487
+ })
488
+ if (input === null) return
489
+ const comment = input.trim() || null
490
+
491
+ const { errors } = await client.mutate({
492
+ mutation: gql`
493
+ mutation ($id: String!, $comment: String) {
494
+ releaseScenario(id: $id, comment: $comment) {
495
+ id
496
+ version
497
+ publishState
498
+ }
499
+ }
500
+ `,
501
+ variables: { id: record.id, comment: comment || null }
502
+ })
503
+
504
+ if (!errors) {
505
+ notify({ message: i18next.t('text.info_x_successfully', { x: i18next.t('button.release') }) })
506
+ this.grist.fetch()
507
+ } else {
508
+ notify({ level: 'error', message: errors.map(error => error.message).join('\n') })
509
+ }
510
+ }
511
+
512
+ openVersionHistory(record) {
513
+ openPopup(
514
+ html`
515
+ <scenario-versions
516
+ .scenarioId=${record.id}
517
+ .scenarioName=${record.name}
518
+ .onReverted=${() => this.grist.fetch()}
519
+ @reverted=${() => this.grist.fetch()}
520
+ ></scenario-versions>
521
+ `,
522
+ {
523
+ backdrop: true,
524
+ size: 'medium',
525
+ title: i18next.t('title.version history')
526
+ }
527
+ )
528
+ }
529
+
409
530
  async startScenario(record) { var { data, errors } = await client.mutate({ mutation: gql`
410
- mutation ($scenarioName: String!, $instanceName: String) { startScenario(scenarioName: $scenarioName, instanceName: $instanceName) { state
531
+ mutation ($scenarioName: String!, $instanceName: String, $mode: String) { startScenario(scenarioName: $scenarioName, instanceName: $instanceName, mode: $mode) { state
411
532
  }
412
533
  }
413
534
  `,
414
535
  variables: { scenarioName: record.name,
415
- instanceName: record.name
536
+ instanceName: record.name,
537
+ mode: 'released'
416
538
  }
417
539
  })
418
540
 
@@ -6,6 +6,7 @@ import gql from 'graphql-tag'
6
6
  import { css, html, LitElement, nothing } from 'lit'
7
7
  import { customElement, property, query } from 'lit/decorators.js'
8
8
  import { subscribe } from '@operato/graphql'
9
+ import { i18next } from '@operato/i18n'
9
10
  import { ScrollbarStyles } from '@operato/styles'
10
11
 
11
12
  @customElement('scenario-instance-log-view')
@@ -51,6 +52,14 @@ export class ScenarioInstanceLogView extends LitElement {
51
52
  bottom: 15px;
52
53
  right: 16px;
53
54
  text-decoration: auto;
55
+ display: flex;
56
+ flex-direction: column-reverse;
57
+ gap: 10px;
58
+ align-items: center;
59
+ }
60
+ #start [draft] {
61
+ --md-fab-container-color: var(--md-sys-color-secondary-container);
62
+ --md-fab-icon-color: var(--md-sys-color-on-secondary-container);
54
63
  }
55
64
  `
56
65
  ]
@@ -87,9 +96,23 @@ export class ScenarioInstanceLogView extends LitElement {
87
96
  )}
88
97
  </div>
89
98
  ${this.startable
90
- ? html`<md-fab id="start" title="start" @click=${() => this.dispatchEvent(new CustomEvent('start'))}>
91
- <md-icon slot="icon">play_arrow</md-icon>
92
- </md-fab>`
99
+ ? html`
100
+ <div id="start">
101
+ <md-fab
102
+ title=${i18next.t('button.run released version')}
103
+ @click=${() => this.dispatchEvent(new CustomEvent('start', { detail: { mode: 'released' } }))}
104
+ >
105
+ <md-icon slot="icon">play_arrow</md-icon>
106
+ </md-fab>
107
+ <md-fab
108
+ draft
109
+ title=${i18next.t('button.run draft for debugging')}
110
+ @click=${() => this.dispatchEvent(new CustomEvent('start', { detail: { mode: 'draft' } }))}
111
+ >
112
+ <md-icon slot="icon">bug_report</md-icon>
113
+ </md-fab>
114
+ </div>
115
+ `
93
116
  : nothing}
94
117
  `
95
118
  }
@@ -243,7 +243,19 @@ export class ScenarioMonitor extends localize(i18next)(LitElement) {
243
243
  <div buttons ?detail=${this.mode == 'detail'}>
244
244
  <md-icon @click=${e => this.popupLogView(scenario)}>text_snippet</md-icon>
245
245
  ${!scenarioInstance
246
- ? html` <md-icon @click=${e => this.startScenario(scenario)}>play_circle</md-icon>`
246
+ ? html`
247
+ <md-icon
248
+ title=${i18next.t('button.run released version')}
249
+ @click=${e => this.startScenario(scenario, 'released')}
250
+ >play_circle</md-icon
251
+ >
252
+ <md-icon
253
+ title=${i18next.t('button.run draft for debugging')}
254
+ @click=${e => this.startScenario(scenario, 'draft')}
255
+ debug
256
+ >bug_report</md-icon
257
+ >
258
+ `
247
259
  : html` <md-icon @click=${e => this.stopScenario(scenario)} stop>pause_circle</md-icon> `}
248
260
  </div>
249
261
  `
@@ -254,7 +266,7 @@ export class ScenarioMonitor extends localize(i18next)(LitElement) {
254
266
  html`
255
267
  <scenario-instance-log-view
256
268
  .scenarioName=${scenario.name}
257
- @start=${() => this.startScenario(scenario)}
269
+ @start=${(e: CustomEvent) => this.startScenario(scenario, e.detail?.mode || 'released')}
258
270
  startable
259
271
  ></scenario-instance-log-view>
260
272
  `,
@@ -265,23 +277,24 @@ export class ScenarioMonitor extends localize(i18next)(LitElement) {
265
277
  )
266
278
  }
267
279
 
268
- async startScenario(scenario) {
280
+ async startScenario(scenario, mode: string = 'released') {
269
281
  var response = await client.mutate({
270
282
  mutation: gql`
271
- mutation ($scenarioName: String!) {
272
- startScenario(scenarioName: $scenarioName) {
283
+ mutation ($scenarioName: String!, $mode: String) {
284
+ startScenario(scenarioName: $scenarioName, mode: $mode) {
273
285
  state
274
286
  }
275
287
  }
276
288
  `,
277
289
  variables: {
278
- scenarioName: scenario.name
290
+ scenarioName: scenario.name,
291
+ mode
279
292
  }
280
293
  })
281
294
 
282
295
  notify({
283
296
  level: 'info',
284
- message: `${!IS_SCENARIO_RUNNING(response.data.startScenario.state) ? 'fail' : 'success'} to start instance : ${scenario.name}`
297
+ message: `${!IS_SCENARIO_RUNNING(response.data.startScenario.state) ? 'fail' : 'success'} to start instance : ${scenario.name} (${mode})`
285
298
  })
286
299
  }
287
300
 
@@ -0,0 +1,334 @@
1
+ import '@material/web/icon/icon.js'
2
+ import '@material/web/chips/assist-chip.js'
3
+ import '@operato/data-grist/ox-empty-note.js'
4
+
5
+ import gql from 'graphql-tag'
6
+ import { css, html, LitElement } from 'lit'
7
+ import { customElement, property, state } from 'lit/decorators.js'
8
+
9
+ import { i18next } from '@operato/i18n'
10
+ import { client } from '@operato/graphql'
11
+ import { notify } from '@operato/layout'
12
+ import { OxPrompt } from '@operato/popup/ox-prompt.js'
13
+ import { ScrollbarStyles } from '@operato/styles'
14
+
15
+ /**
16
+ * 시나리오 버전 히스토리 패널.
17
+ *
18
+ * - 릴리즈된 버전 목록(버전번호·코멘트·작성자·시각)
19
+ * - 각 버전을 현재 draft 와 비교하는 diff 뷰
20
+ * - 과거 버전을 현재 draft 로 복사(revert)
21
+ *
22
+ * board-ui 의 board-versions 를 시나리오(스텝 배열 diff)에 맞춰 확장한 형태.
23
+ */
24
+ @customElement('scenario-versions')
25
+ export class ScenarioVersions extends LitElement {
26
+ static styles = [
27
+ ScrollbarStyles,
28
+ css`
29
+ :host {
30
+ display: flex;
31
+ flex-direction: column;
32
+ overflow: auto;
33
+ background-color: var(--md-sys-color-surface);
34
+ color: var(--md-sys-color-on-surface);
35
+ font: var(--label-font);
36
+ padding: var(--spacing-medium, 12px);
37
+ gap: var(--spacing-medium, 12px);
38
+ }
39
+
40
+ div[card] {
41
+ position: relative;
42
+ border: 1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.12));
43
+ border-radius: var(--border-radius, 6px);
44
+ padding: var(--spacing-medium, 12px);
45
+ text-align: start;
46
+ }
47
+
48
+ div[head] {
49
+ display: flex;
50
+ align-items: center;
51
+ gap: var(--spacing-small, 6px);
52
+ }
53
+
54
+ div[head] [ver] {
55
+ font: var(--subtitle-font, bold 14px sans-serif);
56
+ color: var(--md-sys-color-primary);
57
+ display: inline-flex;
58
+ align-items: center;
59
+ gap: 3px;
60
+ }
61
+
62
+ div[head] [meta] {
63
+ margin-left: auto;
64
+ display: inline-flex;
65
+ align-items: center;
66
+ gap: 5px;
67
+ opacity: 0.75;
68
+ font-size: var(--fontsize-small, 12px);
69
+ }
70
+
71
+ div[comment] {
72
+ margin-top: 6px;
73
+ white-space: pre-wrap;
74
+ color: var(--md-sys-color-on-surface-variant, inherit);
75
+ }
76
+
77
+ div[actions] {
78
+ display: flex;
79
+ gap: var(--spacing-small, 6px);
80
+ margin-top: 8px;
81
+ }
82
+
83
+ md-icon {
84
+ font-size: var(--fontsize-large, 18px);
85
+ vertical-align: middle;
86
+ }
87
+
88
+ button {
89
+ cursor: pointer;
90
+ border: 1px solid var(--md-sys-color-outline, rgba(0, 0, 0, 0.2));
91
+ background: var(--md-sys-color-surface);
92
+ color: inherit;
93
+ border-radius: var(--border-radius, 6px);
94
+ padding: 4px 10px;
95
+ display: inline-flex;
96
+ align-items: center;
97
+ gap: 4px;
98
+ font: inherit;
99
+ }
100
+ button[primary] {
101
+ background: var(--md-sys-color-primary);
102
+ color: var(--md-sys-color-on-primary);
103
+ border-color: transparent;
104
+ }
105
+
106
+ div[diff] {
107
+ margin-top: 8px;
108
+ border-top: 1px dashed var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.12));
109
+ padding-top: 8px;
110
+ font-size: var(--fontsize-small, 12px);
111
+ }
112
+ div[diff] [group] {
113
+ font-weight: bold;
114
+ margin: 4px 0 2px;
115
+ }
116
+ [change] {
117
+ display: block;
118
+ padding: 1px 0;
119
+ }
120
+ [change][added] {
121
+ color: var(--md-sys-color-primary, green);
122
+ }
123
+ [change][removed] {
124
+ color: var(--md-sys-color-error, #b00020);
125
+ text-decoration: line-through;
126
+ opacity: 0.8;
127
+ }
128
+ [change][modified] {
129
+ color: var(--status-warning-color, #b26a00);
130
+ }
131
+ [change] small {
132
+ opacity: 0.7;
133
+ }
134
+
135
+ ox-empty-note {
136
+ padding: 24px;
137
+ opacity: 0.7;
138
+ }
139
+ `
140
+ ]
141
+
142
+ @property({ type: String }) scenarioId?: string
143
+ @property({ type: String }) scenarioName?: string
144
+ /** revert(과거→draft 복사) 성공 시 호출 — 목록 자동 갱신용 콜백(속성 바인딩이라 팝업 경계에서도 확실히 동작). */
145
+ @property({ attribute: false }) onReverted?: () => void
146
+
147
+ @state() versions: any[] = []
148
+ /** version → diff 결과(펼쳐진 경우만 존재) */
149
+ @state() diffs: { [version: number]: any } = {}
150
+
151
+ render() {
152
+ return html`
153
+ ${this.versions.length == 0
154
+ ? /* 빈 상태는 프레임워크 공통 부품(ox-empty-note) 사용 */
155
+ html`<ox-empty-note icon="history" title=${i18next.t('text.no released version information available')}></ox-empty-note>`
156
+ : this.versions.map(
157
+ (version, index) => html`
158
+ <div card>
159
+ <div head>
160
+ <span ver><md-icon>sell</md-icon> v${version.version}</span>
161
+ <span meta>
162
+ <md-icon>person</md-icon> ${version.updater?.name || 'anonymous'}
163
+ <md-icon>schedule</md-icon> ${new Date(version.updatedAt).toLocaleString()}
164
+ </span>
165
+ </div>
166
+
167
+ ${version.comment ? html`<div comment>${version.comment}</div>` : ''}
168
+
169
+ <div actions>
170
+ <button @click=${() => this.toggleDiff(version.version, this.versions[index + 1]?.version)}>
171
+ <md-icon>difference</md-icon>${i18next.t('button.diff with previous')}
172
+ </button>
173
+ <button primary @click=${() => this.revert(version.version)}>
174
+ <md-icon>settings_backup_restore</md-icon>${i18next.t('button.copy to draft')}
175
+ </button>
176
+ </div>
177
+
178
+ ${this.diffs[version.version] ? this.renderDiff(this.diffs[version.version]) : ''}
179
+ </div>
180
+ `
181
+ )}
182
+ `
183
+ }
184
+
185
+ private renderDiff(diff: any) {
186
+ if (diff.earliest) {
187
+ return html`<div diff>${i18next.t('text.initial version all new')}</div>`
188
+ }
189
+
190
+ const fieldDiffs = diff.fieldDiffs || []
191
+ const stepDiffs = diff.stepDiffs || []
192
+
193
+ if (fieldDiffs.length == 0 && stepDiffs.length == 0) {
194
+ return html`<div diff>${i18next.t('text.no difference from previous')}</div>`
195
+ }
196
+
197
+ const fmt = (v: any) => (v === null || v === undefined ? '∅' : typeof v === 'object' ? JSON.stringify(v) : String(v))
198
+
199
+ return html`
200
+ <div diff>
201
+ ${fieldDiffs.length
202
+ ? html`
203
+ <div group>${i18next.t('label.scenario fields')}</div>
204
+ ${fieldDiffs.map(
205
+ (f: any) => html`<span change modified>${f.field}: <small>${fmt(f.before)} → ${fmt(f.after)}</small></span>`
206
+ )}
207
+ `
208
+ : ''}
209
+ ${stepDiffs.length
210
+ ? html`
211
+ <div group>${i18next.t('label.steps')}</div>
212
+ ${stepDiffs.map((s: any) => {
213
+ if (s.changeType == 'added') {
214
+ return html`<span change added>+ ${s.name || '(unnamed)'}</span>`
215
+ }
216
+ if (s.changeType == 'removed') {
217
+ return html`<span change removed>− ${s.name || '(unnamed)'}</span>`
218
+ }
219
+ const changed = (s.fieldDiffs || []).map((fd: any) => fd.field).join(', ')
220
+ return html`<span change modified>~ ${s.name || '(unnamed)'} <small>(${changed})</small></span>`
221
+ })}
222
+ `
223
+ : ''}
224
+ </div>
225
+ `
226
+ }
227
+
228
+ firstUpdated() {
229
+ this.refresh()
230
+ }
231
+
232
+ async refresh() {
233
+ if (!this.scenarioId) {
234
+ return
235
+ }
236
+
237
+ const response = (
238
+ await client.query({
239
+ query: gql`
240
+ query FetchScenarioVersions($id: String!) {
241
+ scenarioVersions(id: $id) {
242
+ id
243
+ version
244
+ comment
245
+ updater {
246
+ name
247
+ }
248
+ updatedAt
249
+ }
250
+ }
251
+ `,
252
+ variables: { id: this.scenarioId },
253
+ fetchPolicy: 'no-cache'
254
+ })
255
+ ).data
256
+
257
+ this.versions = response.scenarioVersions || []
258
+ }
259
+
260
+ async toggleDiff(version: number, prevVersion?: number) {
261
+ if (this.diffs[version]) {
262
+ const { [version]: _removed, ...rest } = this.diffs
263
+ this.diffs = rest
264
+ return
265
+ }
266
+
267
+ // 앞 버전과 비교 = "이 버전이 무엇을 바꿨나"(체인지로그). 최초 버전은 앞 버전이 없어 전체 신규.
268
+ if (prevVersion === undefined || prevVersion === null) {
269
+ this.diffs = { ...this.diffs, [version]: { earliest: true } }
270
+ return
271
+ }
272
+
273
+ const response = await client.query({
274
+ query: gql`
275
+ query ScenarioVersionDiff($id: String!, $base: Int!, $to: Int!) {
276
+ scenarioVersionDiff(id: $id, version: $base, toVersion: $to) {
277
+ fromVersion
278
+ toVersion
279
+ fieldDiffs {
280
+ field
281
+ before
282
+ after
283
+ }
284
+ stepDiffs {
285
+ name
286
+ changeType
287
+ fieldDiffs {
288
+ field
289
+ }
290
+ }
291
+ }
292
+ }
293
+ `,
294
+ variables: { id: this.scenarioId, base: prevVersion, to: version },
295
+ fetchPolicy: 'no-cache'
296
+ })
297
+
298
+ this.diffs = { ...this.diffs, [version]: response.data.scenarioVersionDiff }
299
+ }
300
+
301
+ async revert(version: number) {
302
+ const ok = await OxPrompt.open({
303
+ type: 'question',
304
+ title: String(i18next.t('button.copy to draft')),
305
+ text: String(i18next.t('text.sure to copy version to draft', { version })),
306
+ confirmButton: { text: String(i18next.t('button.confirm')) },
307
+ cancelButton: { text: String(i18next.t('button.cancel')) }
308
+ })
309
+ if (!ok) {
310
+ return
311
+ }
312
+
313
+ const response = await client.mutate({
314
+ mutation: gql`
315
+ mutation RevertScenarioVersion($id: String!, $version: Int!) {
316
+ revertScenarioVersion(id: $id, version: $version) {
317
+ id
318
+ version
319
+ publishState
320
+ }
321
+ }
322
+ `,
323
+ variables: { id: this.scenarioId, version }
324
+ })
325
+
326
+ if (!response.errors) {
327
+ notify({ message: i18next.t('text.info_x_successfully', { x: i18next.t('text.copy') }) })
328
+ this.onReverted?.()
329
+ this.dispatchEvent(new CustomEvent('reverted', { bubbles: true, composed: true }))
330
+ } else {
331
+ notify({ level: 'error', message: response.errors.map(e => e.message).join('\n') })
332
+ }
333
+ }
334
+ }
@@ -1,6 +1,8 @@
1
+ import '@material/web/icon/icon.js';
1
2
  import '@operato/data-grist';
2
3
  import './scenario-detail.js';
3
4
  import './scenario-importer.js';
5
+ import '../viewparts/scenario-versions.js';
4
6
  import { DataGrist } from '@operato/data-grist/ox-grist.js';
5
7
  import { PageView } from '@operato/shell';
6
8
  import { FetchOption } from '@operato/data-grist';
@@ -53,6 +55,8 @@ export declare class Scenario extends Scenario_base {
53
55
  _deleteScenario(): Promise<void>;
54
56
  _copyScenario(): Promise<void>;
55
57
  _updateScenario(): Promise<void>;
58
+ releaseScenario(record: any): Promise<void>;
59
+ openVersionHistory(record: any): void;
56
60
  startScenario(record: any): Promise<void>;
57
61
  stopScenario(record: any): Promise<void>;
58
62
  startScenarioSchedule(record: any): Promise<void>;