@finos/legend-application-studio 28.19.70 → 28.19.72

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.
Files changed (26) hide show
  1. package/lib/components/editor/editor-group/dataProduct/DataProductEditor.d.ts +5 -0
  2. package/lib/components/editor/editor-group/dataProduct/DataProductEditor.d.ts.map +1 -1
  3. package/lib/components/editor/editor-group/dataProduct/DataProductEditor.js +38 -5
  4. package/lib/components/editor/editor-group/dataProduct/DataProductEditor.js.map +1 -1
  5. package/lib/components/editor/side-bar/DevMetadataPanel.d.ts.map +1 -1
  6. package/lib/components/editor/side-bar/DevMetadataPanel.js +150 -32
  7. package/lib/components/editor/side-bar/DevMetadataPanel.js.map +1 -1
  8. package/lib/index.css +2 -2
  9. package/lib/index.css.map +1 -1
  10. package/lib/package.json +5 -4
  11. package/lib/stores/editor/editor-state/element-editor-state/dataProduct/DataProductEditorState.d.ts +11 -1
  12. package/lib/stores/editor/editor-state/element-editor-state/dataProduct/DataProductEditorState.d.ts.map +1 -1
  13. package/lib/stores/editor/editor-state/element-editor-state/dataProduct/DataProductEditorState.js +25 -2
  14. package/lib/stores/editor/editor-state/element-editor-state/dataProduct/DataProductEditorState.js.map +1 -1
  15. package/lib/stores/editor/sidebar-state/dev-metadata/DevMetadataState.d.ts +8 -5
  16. package/lib/stores/editor/sidebar-state/dev-metadata/DevMetadataState.d.ts.map +1 -1
  17. package/lib/stores/editor/sidebar-state/dev-metadata/DevMetadataState.js +18 -28
  18. package/lib/stores/editor/sidebar-state/dev-metadata/DevMetadataState.js.map +1 -1
  19. package/lib/stores/graph-modifier/DSL_DataProduct_GraphModifierHelper.d.ts +1 -1
  20. package/lib/stores/graph-modifier/DSL_DataProduct_GraphModifierHelper.d.ts.map +1 -1
  21. package/package.json +14 -13
  22. package/src/components/editor/editor-group/dataProduct/DataProductEditor.tsx +131 -4
  23. package/src/components/editor/side-bar/DevMetadataPanel.tsx +613 -73
  24. package/src/stores/editor/editor-state/element-editor-state/dataProduct/DataProductEditorState.ts +35 -0
  25. package/src/stores/editor/sidebar-state/dev-metadata/DevMetadataState.ts +28 -39
  26. package/src/stores/graph-modifier/DSL_DataProduct_GraphModifierHelper.ts +1 -1
@@ -16,7 +16,7 @@
16
16
 
17
17
  import { observer } from 'mobx-react-lite';
18
18
  import { useEditorStore } from '../EditorStoreProvider.js';
19
- import { useEffect } from 'react';
19
+ import { forwardRef, useEffect, useState, type ReactNode } from 'react';
20
20
  import {
21
21
  PanelFormSection,
22
22
  PanelFormValidatedTextField,
@@ -32,56 +32,363 @@ import {
32
32
  ModalHeader,
33
33
  Modal,
34
34
  ModalTitle,
35
+ PanelDivider,
36
+ CircleNotchIcon,
37
+ PauseCircleIcon,
38
+ TimesCircleIcon,
39
+ BanIcon,
40
+ QuestionCircleIcon,
41
+ MenuContent,
42
+ MenuContentItem,
43
+ ContextMenu,
44
+ PanelForm,
45
+ PanelFormBooleanField,
46
+ TrashIcon,
47
+ PlusIcon,
48
+ CogIcon,
35
49
  } from '@finos/legend-art';
36
50
  import { CODE_EDITOR_LANGUAGE } from '@finos/legend-code-editor';
37
51
  import { CodeEditor } from '@finos/legend-lego/code-editor';
38
- import { DevMetadataResult } from '@finos/legend-graph';
52
+ import {
53
+ type BuildLog,
54
+ type BuildPhaseActionState,
55
+ BuildPhaseStatus,
56
+ type DeployProjectResponse,
57
+ LogType,
58
+ MetadataRequestOptions,
59
+ } from '@finos/legend-graph';
60
+ import { flowResult } from 'mobx';
39
61
 
40
- const DevMetadataResultModal = observer(
62
+ const BuildOverrideEditor = observer(
41
63
  (props: {
42
- closeModal: () => void;
43
- deploymentResponse: DevMetadataResult;
64
+ buildOverrides: Record<string, string>;
65
+ onUpdate: (overrides: Record<string, string>) => void;
44
66
  }) => {
45
- const { closeModal, deploymentResponse } = props;
67
+ const { buildOverrides, onUpdate } = props;
68
+ const [newKey, setNewKey] = useState('');
69
+ const [newValue, setNewValue] = useState('');
70
+
71
+ const handleAddOverride = () => {
72
+ if (newKey.trim() && newValue.trim()) {
73
+ onUpdate({
74
+ ...buildOverrides,
75
+ [newKey.trim()]: newValue.trim(),
76
+ });
77
+ setNewKey('');
78
+ setNewValue('');
79
+ }
80
+ };
81
+
82
+ const handleDeleteOverride = (key: string) => {
83
+ const updatedOverrides = { ...buildOverrides };
84
+ delete updatedOverrides[key];
85
+ onUpdate(updatedOverrides);
86
+ };
87
+
88
+ const handleUpdateOverride = (key: string, value: string) => {
89
+ onUpdate({
90
+ ...buildOverrides,
91
+ [key]: value,
92
+ });
93
+ };
94
+
95
+ return (
96
+ <div className="build-override-editor">
97
+ <div className="build-override-editor__header">
98
+ <span className="build-override-editor__title">Build Overrides</span>
99
+ <span className="build-override-editor__description">
100
+ Key-value pairs to override build parameters
101
+ </span>
102
+ </div>
103
+
104
+ {/* Existing overrides */}
105
+ <div className="build-override-editor__list">
106
+ {Object.entries(buildOverrides).map(([key, value]) => (
107
+ <div key={key} className="build-override-editor__item">
108
+ <div className="build-override-editor__item__fields">
109
+ <PanelFormValidatedTextField
110
+ name={`override-key-${key}`}
111
+ prompt="Key"
112
+ value={key}
113
+ update={(val) => {
114
+ if (val && val !== key && val.trim()) {
115
+ const newOverrides = { ...buildOverrides };
116
+ delete newOverrides[key];
117
+ newOverrides[val.trim()] = value;
118
+ onUpdate(newOverrides);
119
+ }
120
+ }}
121
+ />
122
+ <PanelFormValidatedTextField
123
+ name={`override-value-${key}`}
124
+ prompt="Value"
125
+ value={value}
126
+ update={(val) => handleUpdateOverride(key, val ?? '')}
127
+ />
128
+ </div>
129
+ <button
130
+ className="build-override-editor__delete-btn"
131
+ onClick={() => handleDeleteOverride(key)}
132
+ title="Delete override"
133
+ >
134
+ <TrashIcon />
135
+ </button>
136
+ </div>
137
+ ))}
138
+ </div>
139
+
140
+ {/* Add new override */}
141
+ <div className="build-override-editor__add">
142
+ <div className="build-override-editor__add__title">
143
+ Add New Override
144
+ </div>
145
+ <div className="build-override-editor__add__fields">
146
+ <PanelFormValidatedTextField
147
+ name="new-override-key"
148
+ prompt="Key"
149
+ value={newKey}
150
+ update={(val: string | undefined) => setNewKey(val ?? '')}
151
+ />
152
+ <PanelFormValidatedTextField
153
+ name="new-override-value"
154
+ prompt="Value"
155
+ value={newValue}
156
+ update={(val: string | undefined) => setNewValue(val ?? '')}
157
+ />
158
+ <button
159
+ className="build-override-editor__add-btn"
160
+ onClick={handleAddOverride}
161
+ disabled={!newKey.trim() || !newValue.trim()}
162
+ title="Add override"
163
+ >
164
+ <PlusIcon />
165
+ </button>
166
+ </div>
167
+ </div>
168
+ </div>
169
+ );
170
+ },
171
+ );
172
+
173
+ const DevMetadataOptionsModal = observer(
174
+ (props: {
175
+ isOpen: boolean;
176
+ onClose: () => void;
177
+ options: MetadataRequestOptions;
178
+ onSave: (options: MetadataRequestOptions) => void;
179
+ }) => {
180
+ const { isOpen, onClose, options, onSave } = props;
181
+ const editorStore = useEditorStore();
182
+ const applicationStore = editorStore.applicationStore;
183
+
184
+ const [includeArtifacts, setIncludeArtifacts] = useState(
185
+ options.includeArtifacts ?? false,
186
+ );
187
+ const [buildOverrides, setBuildOverrides] = useState(
188
+ options.buildOverrides ?? {},
189
+ );
190
+
191
+ useEffect(() => {
192
+ if (isOpen) {
193
+ setIncludeArtifacts(options.includeArtifacts ?? false);
194
+ setBuildOverrides(options.buildOverrides ?? {});
195
+ }
196
+ }, [isOpen, options]);
197
+
198
+ const handleSave = () => {
199
+ const updatedOptions = new MetadataRequestOptions();
200
+ updatedOptions.includeArtifacts = includeArtifacts;
201
+ updatedOptions.buildOverrides =
202
+ Object.keys(buildOverrides).length > 0 ? buildOverrides : undefined;
203
+ onSave(updatedOptions);
204
+ onClose();
205
+ };
206
+
207
+ const handleCancel = () => {
208
+ setIncludeArtifacts(options.includeArtifacts ?? false);
209
+ setBuildOverrides(options.buildOverrides ?? {});
210
+ onClose();
211
+ };
212
+
213
+ if (!isOpen) {
214
+ return null;
215
+ }
216
+
217
+ return (
218
+ <Dialog
219
+ open={isOpen}
220
+ onClose={handleCancel}
221
+ classes={{
222
+ root: 'editor-modal__root-container',
223
+ container: 'editor-modal__container',
224
+ paper: 'editor-modal__content',
225
+ }}
226
+ >
227
+ <Modal
228
+ darkMode={
229
+ !applicationStore.layoutService.TEMPORARY__isLightColorThemeEnabled
230
+ }
231
+ className="editor-modal dev-metadata-options-modal"
232
+ >
233
+ <ModalHeader>
234
+ <ModalTitle title="Deployment Configuration" />
235
+ </ModalHeader>
236
+ <ModalBody className="dev-metadata-options-modal__body">
237
+ <PanelForm>
238
+ <PanelFormSection>
239
+ <div className="panel__content__form__section__header__label">
240
+ Artifact Settings
241
+ </div>
242
+ <PanelFormBooleanField
243
+ name="includeArtifacts"
244
+ prompt="Include Artifacts"
245
+ value={includeArtifacts}
246
+ update={(val: boolean | undefined) =>
247
+ setIncludeArtifacts(Boolean(val))
248
+ }
249
+ isReadOnly={false}
250
+ />
251
+ <div className="panel__content__form__section__desc">
252
+ When enabled, includes generated artifacts in the deployment
253
+ </div>
254
+ </PanelFormSection>
255
+
256
+ <PanelFormSection>
257
+ <BuildOverrideEditor
258
+ buildOverrides={buildOverrides}
259
+ onUpdate={setBuildOverrides}
260
+ />
261
+ </PanelFormSection>
262
+ </PanelForm>
263
+ </ModalBody>
264
+ <ModalFooter>
265
+ <ModalFooterButton
266
+ text="Cancel"
267
+ onClick={handleCancel}
268
+ type="secondary"
269
+ />
270
+ <ModalFooterButton
271
+ text="Save Configuration"
272
+ onClick={handleSave}
273
+ type="primary"
274
+ />
275
+ </ModalFooter>
276
+ </Modal>
277
+ </Dialog>
278
+ );
279
+ },
280
+ );
281
+
282
+ const getPhaseStatusIcon = (status: BuildPhaseStatus): ReactNode => {
283
+ switch (status) {
284
+ case BuildPhaseStatus.NOT_STARTED:
285
+ return (
286
+ <div
287
+ title="Phase not started"
288
+ className="deployment-phase__status__indicator deployment-phase__status__indicator--not-started"
289
+ >
290
+ <PauseCircleIcon />
291
+ </div>
292
+ );
293
+ case BuildPhaseStatus.IN_PROGRESS:
294
+ return (
295
+ <div
296
+ title="Phase in progress"
297
+ className="deployment-phase__status__indicator deployment-phase__status__indicator--in-progress"
298
+ >
299
+ <CircleNotchIcon />
300
+ </div>
301
+ );
302
+ case BuildPhaseStatus.SUCCESS:
303
+ return (
304
+ <div
305
+ title="Phase succeeded"
306
+ className="deployment-phase__status__indicator deployment-phase__status__indicator--success"
307
+ >
308
+ <CheckCircleIcon />
309
+ </div>
310
+ );
311
+ case BuildPhaseStatus.FAIL:
312
+ return (
313
+ <div
314
+ title="Phase failed"
315
+ className="deployment-phase__status__indicator deployment-phase__status__indicator--failed"
316
+ >
317
+ <TimesCircleIcon />
318
+ </div>
319
+ );
320
+ case BuildPhaseStatus.SKIPPED:
321
+ return (
322
+ <div
323
+ title="Phase skipped"
324
+ className="deployment-phase__status__indicator deployment-phase__status__indicator--skipped"
325
+ >
326
+ <BanIcon />
327
+ </div>
328
+ );
329
+ default:
330
+ return (
331
+ <div
332
+ title="Phase status unknown"
333
+ className="deployment-phase__status__indicator deployment-phase__status__indicator--unknown"
334
+ >
335
+ <QuestionCircleIcon />
336
+ </div>
337
+ );
338
+ }
339
+ };
340
+
341
+ const PhaseLogsViewer = observer(
342
+ (props: { phase: BuildPhaseActionState; onClose: () => void }) => {
343
+ const { phase, onClose } = props;
344
+ const editorStore = useEditorStore();
345
+ const applicationStore = editorStore.applicationStore;
346
+
347
+ const formatLogs = (logs: BuildLog[]): string => {
348
+ return logs
349
+ .map((log) => {
350
+ const prefix = log.logType ? `[${log.logType}]` : '';
351
+ const title = log.title ? `${log.title}: ` : '';
352
+ return `${prefix} ${title}${log.log}`;
353
+ })
354
+ .join('\n');
355
+ };
356
+
357
+ const logs = phase.logs ? formatLogs(phase.logs) : 'No logs available';
358
+
46
359
  return (
47
360
  <Dialog
48
361
  open={true}
362
+ onClose={onClose}
49
363
  classes={{
50
- root: 'ingestion-modal__root-container',
51
- container: 'ingestion-modal__container',
52
- paper: 'ingestion-modal__content',
364
+ root: 'editor-modal__root-container',
365
+ container: 'editor-modal__container',
366
+ paper: 'editor-modal__content',
53
367
  }}
54
368
  >
55
- <Modal darkMode={true} className="ingestion-modal">
369
+ <Modal
370
+ darkMode={
371
+ !applicationStore.layoutService.TEMPORARY__isLightColorThemeEnabled
372
+ }
373
+ className="editor-modal"
374
+ >
56
375
  <ModalHeader>
57
- <ModalTitle
58
- icon={<CheckCircleIcon className="ingestion-modal--success" />}
59
- title="Push Response"
60
- ></ModalTitle>
376
+ <ModalTitle title={`Logs for Phase: ${phase.phase}`} />
61
377
  </ModalHeader>
62
378
  <ModalBody>
63
- <div className="ingestion-modal__write">
64
- <div className="ingestion-modal__write--value">
65
- <CodeEditor
66
- inputValue={JSON.stringify(
67
- DevMetadataResult.serialization.toJson(deploymentResponse),
68
- null,
69
- 2,
70
- )}
71
- isReadOnly={true}
72
- language={CODE_EDITOR_LANGUAGE.JSON}
73
- extraEditorOptions={{
74
- wordWrap: 'on',
75
- }}
76
- hideActionBar={true}
77
- />
78
- </div>
79
- </div>
379
+ <CodeEditor
380
+ inputValue={logs}
381
+ isReadOnly={true}
382
+ language={CODE_EDITOR_LANGUAGE.TEXT}
383
+ extraEditorOptions={{
384
+ wordWrap: 'on',
385
+ }}
386
+ />
80
387
  </ModalBody>
81
388
  <ModalFooter>
82
389
  <ModalFooterButton
83
- onClick={closeModal}
84
390
  text="Close"
391
+ onClick={onClose}
85
392
  type="secondary"
86
393
  />
87
394
  </ModalFooter>
@@ -91,23 +398,198 @@ const DevMetadataResultModal = observer(
91
398
  },
92
399
  );
93
400
 
401
+ const PhaseContextMenu = observer(
402
+ forwardRef<
403
+ HTMLDivElement,
404
+ {
405
+ phase: BuildPhaseActionState;
406
+ onViewLogs: () => void;
407
+ }
408
+ >(function PhaseContextMenu(props, ref) {
409
+ const { phase, onViewLogs } = props;
410
+ const hasLogs = phase.logs && phase.logs.length > 0;
411
+
412
+ return (
413
+ <MenuContent>
414
+ {hasLogs && (
415
+ <MenuContentItem onClick={onViewLogs}>
416
+ View Logs ({phase.logs?.length} entries)
417
+ </MenuContentItem>
418
+ )}
419
+ </MenuContent>
420
+ );
421
+ }),
422
+ );
423
+
424
+ const DeploymentPhaseNode = observer(
425
+ (props: {
426
+ phase: BuildPhaseActionState;
427
+ onViewLogs: (phase: BuildPhaseActionState) => void;
428
+ }) => {
429
+ const { phase, onViewLogs } = props;
430
+ const statusIcon = getPhaseStatusIcon(phase.status);
431
+ const hasLogs = phase.logs && phase.logs.length > 0;
432
+ const errorLogs =
433
+ phase.logs?.filter((log) => log.logType === LogType.ERROR).length ?? 0;
434
+ const warnLogs =
435
+ phase.logs?.filter((log) => log.logType === LogType.WARN).length ?? 0;
436
+
437
+ return (
438
+ <ContextMenu
439
+ content={
440
+ <PhaseContextMenu
441
+ phase={phase}
442
+ onViewLogs={() => onViewLogs(phase)}
443
+ />
444
+ }
445
+ menuProps={{ elevation: 7 }}
446
+ >
447
+ <div className="deployment-phase__node">
448
+ <div className="deployment-phase__node__icon">{statusIcon}</div>
449
+ <div className="deployment-phase__node__content">
450
+ <div className="deployment-phase__node__title">{phase.phase}</div>
451
+ <div className="deployment-phase__node__details">
452
+ <span
453
+ className={`deployment-phase__status deployment-phase__status--${phase.status.toLowerCase()}`}
454
+ >
455
+ {phase.status}
456
+ </span>
457
+ {phase.message && (
458
+ <span className="deployment-phase__message">
459
+ {phase.message}
460
+ </span>
461
+ )}
462
+ {hasLogs && (
463
+ <span className="deployment-phase__logs-count">
464
+ {errorLogs > 0 && (
465
+ <span className="logs-count logs-count--error">
466
+ {errorLogs} errors
467
+ </span>
468
+ )}
469
+ {warnLogs > 0 && (
470
+ <span className="logs-count logs-count--warn">
471
+ {warnLogs} warnings
472
+ </span>
473
+ )}
474
+ <span className="logs-count logs-count--total">
475
+ {phase.logs?.length} logs
476
+ </span>
477
+ </span>
478
+ )}
479
+ </div>
480
+ </div>
481
+ </div>
482
+ </ContextMenu>
483
+ );
484
+ },
485
+ );
486
+
487
+ const DeploymentStatusPanel = observer(
488
+ (props: { deploymentResponse: DeployProjectResponse }) => {
489
+ const { deploymentResponse } = props;
490
+ const [selectedPhaseForLogs, setSelectedPhaseForLogs] =
491
+ useState<BuildPhaseActionState | null>(null);
492
+
493
+ const handleViewLogs = (phase: BuildPhaseActionState) => {
494
+ setSelectedPhaseForLogs(phase);
495
+ };
496
+
497
+ const closeLogsViewer = () => {
498
+ setSelectedPhaseForLogs(null);
499
+ };
500
+
501
+ return (
502
+ <div className="deployment-status-panel">
503
+ <div className="deployment-status-panel__header">
504
+ <div className="deployment-status-panel__title">
505
+ Deployment Status
506
+ </div>
507
+ <div
508
+ className={`deployment-status-panel__final-status deployment-status-panel__final-status--${deploymentResponse.finalStatus.toLowerCase()}`}
509
+ >
510
+ {getPhaseStatusIcon(deploymentResponse.finalStatus)}
511
+ <span>{deploymentResponse.finalStatus}</span>
512
+ </div>
513
+ </div>
514
+
515
+ <PanelDivider />
516
+
517
+ {/* Project Details */}
518
+ <div className="deployment-status-panel__project-details">
519
+ <div className="deployment-status-panel__section-title">
520
+ Project Details
521
+ </div>
522
+ <div className="deployment-status-panel__details-grid">
523
+ <div className="detail-item">
524
+ <span className="detail-label">Group ID:</span>
525
+ <span className="detail-value">
526
+ {deploymentResponse.projectDetails.groupId}
527
+ </span>
528
+ </div>
529
+ <div className="detail-item">
530
+ <span className="detail-label">Artifact ID:</span>
531
+ <span className="detail-value">
532
+ {deploymentResponse.projectDetails.artifactId}
533
+ </span>
534
+ </div>
535
+ <div className="detail-item">
536
+ <span className="detail-label">Version:</span>
537
+ <span className="detail-value">
538
+ {deploymentResponse.projectDetails.version}
539
+ </span>
540
+ </div>
541
+ </div>
542
+ </div>
543
+
544
+ <PanelDivider />
545
+
546
+ {/* Phases */}
547
+ {deploymentResponse.phaseStates &&
548
+ deploymentResponse.phaseStates.length > 0 && (
549
+ <div className="deployment-status-panel__phases">
550
+ <div className="deployment-status-panel__section-title">
551
+ Deployment Phases ({deploymentResponse.phaseStates.length})
552
+ </div>
553
+ <div className="deployment-phases-list">
554
+ {deploymentResponse.phaseStates.map((phase) => (
555
+ <DeploymentPhaseNode
556
+ key={`${phase.phase}`}
557
+ phase={phase}
558
+ onViewLogs={handleViewLogs}
559
+ />
560
+ ))}
561
+ </div>
562
+ </div>
563
+ )}
564
+
565
+ {/* Logs Viewer Modal */}
566
+ {selectedPhaseForLogs && (
567
+ <PhaseLogsViewer
568
+ phase={selectedPhaseForLogs}
569
+ onClose={closeLogsViewer}
570
+ />
571
+ )}
572
+ </div>
573
+ );
574
+ },
575
+ );
576
+
94
577
  export const DevMetadataPanel = observer(() => {
95
578
  const editorStore = useEditorStore();
96
579
  const devMetadataState = editorStore.devMetadataState;
97
-
98
- useEffect(() => {
99
- devMetadataState.init();
100
- }, [devMetadataState]);
580
+ const [isOptionsModalOpen, setIsOptionsModalOpen] = useState(false);
101
581
 
102
582
  const handlePush = (): void => {
103
- devMetadataState.push();
583
+ flowResult(devMetadataState.push()).catch(
584
+ editorStore.applicationStore.alertUnhandledError,
585
+ );
104
586
  };
105
587
 
106
- const handleDidChange = (value: string | undefined): void => {
107
- devMetadataState.setDid(value ?? '');
588
+ const handleSaveOptions = (newOptions: MetadataRequestOptions): void => {
589
+ devMetadataState.setOptions(newOptions);
108
590
  };
109
591
 
110
- const isPushDisabled = !devMetadataState.did.trim();
592
+ const isPushing = devMetadataState.pushState.isInProgress;
111
593
 
112
594
  return (
113
595
  <Panel>
@@ -119,50 +601,108 @@ export const DevMetadataPanel = observer(() => {
119
601
  </div>
120
602
  </PanelHeader>
121
603
  <PanelContent>
122
- <form
123
- onSubmit={(event) => {
124
- event.preventDefault();
125
- if (!isPushDisabled) {
126
- handlePush();
127
- }
128
- }}
129
- >
130
- <PanelFormSection>
131
- <div className="panel__content__form__section__header__label">
132
- DID
604
+ <PanelFormSection>
605
+ <div className="dev-metadata-panel__project-info">
606
+ <div className="dev-metadata-panel__info-header">
607
+ Project Information
608
+ </div>
609
+ <div className="dev-metadata-panel__info-content">
610
+ <div className="dev-metadata-panel__info-row">
611
+ <span className="dev-metadata-panel__info-label">
612
+ Group ID:
613
+ </span>
614
+ <span className="dev-metadata-panel__info-value">
615
+ {devMetadataState.projectGAV?.groupId ?? 'Not available'}
616
+ </span>
617
+ </div>
618
+ <div className="dev-metadata-panel__info-row">
619
+ <span className="dev-metadata-panel__info-label">
620
+ Artifact ID:
621
+ </span>
622
+ <span className="dev-metadata-panel__info-value">
623
+ {devMetadataState.projectGAV?.artifactId ?? 'Not available'}
624
+ </span>
625
+ </div>
626
+ </div>
627
+ </div>
628
+ </PanelFormSection>
629
+
630
+ <PanelDivider />
631
+ <PanelFormSection>
632
+ <div className="dev-metadata-panel__push-section">
633
+ <div className="dev-metadata-panel__push-header">
634
+ <div className="dev-metadata-panel__push-title-row">
635
+ <div className="panel__content__form__section__header__label">
636
+ Deploy Metadata
637
+ </div>
638
+ <button
639
+ className="dev-metadata-panel__settings-btn"
640
+ onClick={() => setIsOptionsModalOpen(true)}
641
+ title="Configure deployment options"
642
+ disabled={isPushing}
643
+ >
644
+ <CogIcon />
645
+ </button>
646
+ </div>
647
+ <div className="dev-metadata-panel__push-description">
648
+ {isPushing
649
+ ? 'Pushing metadata to dev environment...'
650
+ : 'Push current workspace metadata to dev'}
651
+ </div>
133
652
  </div>
134
- <PanelFormValidatedTextField
135
- value={devMetadataState.did}
136
- update={handleDidChange}
137
- placeholder="Enter DID..."
138
- />
139
- </PanelFormSection>
140
- <PanelFormSection>
141
653
  <button
654
+ onClick={handlePush}
142
655
  type="submit"
143
- className={clsx('btn btn--primary register-service__push-btn', {
144
- 'btn--disabled': isPushDisabled,
656
+ className={clsx('btn btn--primary dev-metadata-panel__push-btn', {
657
+ 'btn--loading': isPushing,
658
+ 'btn--disabled': isPushing,
145
659
  })}
146
- disabled={isPushDisabled}
660
+ disabled={isPushing}
147
661
  title={
148
- isPushDisabled
149
- ? 'Please fill in both DID and Project Name'
150
- : 'Push to Dev'
662
+ isPushing
663
+ ? 'Pushing metadata...'
664
+ : 'Push metadata to development environment'
151
665
  }
152
666
  >
153
667
  <div className="btn__content">
154
- <div className="btn__content__label">Push</div>
668
+ {isPushing && (
669
+ <CircleNotchIcon className="dev-metadata-panel__push-btn__spinner" />
670
+ )}
671
+ <div className="btn__content__label">
672
+ {isPushing ? 'Pushing...' : 'Push to Dev'}
673
+ </div>
155
674
  </div>
156
675
  </button>
157
- </PanelFormSection>
158
- </form>
159
- </PanelContent>
160
- {devMetadataState.result && (
161
- <DevMetadataResultModal
162
- closeModal={() => (devMetadataState.result = undefined)}
163
- deploymentResponse={devMetadataState.result}
676
+
677
+ {isPushing && (
678
+ <div className="dev-metadata-panel__loading-overlay">
679
+ <div className="dev-metadata-panel__loading-content">
680
+ <CircleNotchIcon className="dev-metadata-panel__loading-spinner" />
681
+ <div className="dev-metadata-panel__loading-text">
682
+ Deploying metadata to development environment...
683
+ </div>
684
+ </div>
685
+ </div>
686
+ )}
687
+ </div>
688
+ </PanelFormSection>
689
+ <PanelFormSection>
690
+ {devMetadataState.result && (
691
+ <>
692
+ <PanelDivider />
693
+ <DeploymentStatusPanel
694
+ deploymentResponse={devMetadataState.result}
695
+ />
696
+ </>
697
+ )}
698
+ </PanelFormSection>
699
+ <DevMetadataOptionsModal
700
+ isOpen={isOptionsModalOpen}
701
+ onClose={() => setIsOptionsModalOpen(false)}
702
+ options={devMetadataState.options}
703
+ onSave={handleSaveOptions}
164
704
  />
165
- )}
705
+ </PanelContent>
166
706
  </Panel>
167
707
  );
168
708
  });