@epcare/esm-report-builder 0.1.1-pre.13

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 (222) hide show
  1. package/.eslintignore +5 -0
  2. package/.eslintrc.js +19 -0
  3. package/.prettierrc +8 -0
  4. package/LICENSE +401 -0
  5. package/README.md +317 -0
  6. package/dist/184.js +2 -0
  7. package/dist/184.js.LICENSE.txt +14 -0
  8. package/dist/184.js.map +1 -0
  9. package/dist/211.js +1 -0
  10. package/dist/211.js.map +1 -0
  11. package/dist/300.js +1 -0
  12. package/dist/311.js +1 -0
  13. package/dist/311.js.map +1 -0
  14. package/dist/44.js +2 -0
  15. package/dist/44.js.LICENSE.txt +30 -0
  16. package/dist/44.js.map +1 -0
  17. package/dist/478.js +2 -0
  18. package/dist/478.js.LICENSE.txt +9 -0
  19. package/dist/478.js.map +1 -0
  20. package/dist/535.js +1 -0
  21. package/dist/535.js.map +1 -0
  22. package/dist/540.js +2 -0
  23. package/dist/540.js.LICENSE.txt +9 -0
  24. package/dist/540.js.map +1 -0
  25. package/dist/710.js +1 -0
  26. package/dist/710.js.map +1 -0
  27. package/dist/729.js +1 -0
  28. package/dist/729.js.map +1 -0
  29. package/dist/872.js +1 -0
  30. package/dist/872.js.map +1 -0
  31. package/dist/917.js +1 -0
  32. package/dist/917.js.map +1 -0
  33. package/dist/961.js +2 -0
  34. package/dist/961.js.LICENSE.txt +19 -0
  35. package/dist/961.js.map +1 -0
  36. package/dist/972.js +2 -0
  37. package/dist/972.js.LICENSE.txt +21 -0
  38. package/dist/972.js.map +1 -0
  39. package/dist/main.js +1 -0
  40. package/dist/main.js.map +1 -0
  41. package/dist/openmrs-esm-report-builder.js +1 -0
  42. package/dist/openmrs-esm-report-builder.js.buildmanifest.json +482 -0
  43. package/dist/openmrs-esm-report-builder.js.map +1 -0
  44. package/dist/routes.json +1 -0
  45. package/e2e/core/env.ts +5 -0
  46. package/e2e/core/global-setup.ts +34 -0
  47. package/e2e/core/test.ts +21 -0
  48. package/e2e/pages/app-shell.page.ts +34 -0
  49. package/e2e/pages/etl-sources.page.ts +35 -0
  50. package/e2e/pages/report-sections.page.ts +20 -0
  51. package/e2e/pages/section-preview.modal.ts +29 -0
  52. package/e2e/specs/etl-sources.spec.ts +51 -0
  53. package/e2e/specs/navigation.spec.ts +13 -0
  54. package/e2e/specs/section-preview-validation.spec.ts +38 -0
  55. package/e2e/specs/section-preview.spec.ts +94 -0
  56. package/e2e/tsconfig.json +11 -0
  57. package/example.env +5 -0
  58. package/jest.config.js +28 -0
  59. package/package.json +102 -0
  60. package/playwright.config.ts +28 -0
  61. package/src/admin-card-link.extension.module.scss +55 -0
  62. package/src/admin-card-link.extension.tsx +24 -0
  63. package/src/components/admin/admin-page.component.tsx +31 -0
  64. package/src/components/admin/age-categories-page.component.tsx +109 -0
  65. package/src/components/admin/age-groups-page.component.tsx +146 -0
  66. package/src/components/admin/etl-sources-page.component.tsx +344 -0
  67. package/src/components/admin/report-categories-page.component.tsx +256 -0
  68. package/src/components/admin/report-library-page.component.tsx +540 -0
  69. package/src/components/ai-support/ai-assist-button.component.tsx +30 -0
  70. package/src/components/ai-support/ai-assist-modal.component.tsx +81 -0
  71. package/src/components/ai-support/ai.service.ts +21 -0
  72. package/src/components/ai-support/ai.types.ts +12 -0
  73. package/src/components/app-shell/report-builder-shell.component.tsx +120 -0
  74. package/src/components/app-shell/report-builder-shell.scss +15 -0
  75. package/src/components/data-themes/data-theme-form.component.tsx +59 -0
  76. package/src/components/data-themes/data-theme-modal.component.tsx +349 -0
  77. package/src/components/data-themes/data-themes-page.component.tsx +132 -0
  78. package/src/components/data-themes/data-themes-table.component.tsx +91 -0
  79. package/src/components/data-themes/icon-registry.ts +35 -0
  80. package/src/components/data-themes/sections/data-theme-basics.section.tsx +63 -0
  81. package/src/components/data-themes/sections/data-theme-condition-columns.section.tsx +397 -0
  82. package/src/components/data-themes/sections/data-theme-fields-editor.section.tsx +125 -0
  83. package/src/components/data-themes/sections/data-theme-metadata.section.tsx +245 -0
  84. package/src/components/data-themes/sections/data-theme-preview.section.tsx +115 -0
  85. package/src/components/data-themes/sections/data-theme-source.section.tsx +184 -0
  86. package/src/components/indicators/README.md +24 -0
  87. package/src/components/indicators/base-indicator-basic-fields.component.tsx +18 -0
  88. package/src/components/indicators/base-indicator-basics.section.tsx +54 -0
  89. package/src/components/indicators/base-indicator-countby.section.tsx +25 -0
  90. package/src/components/indicators/base-indicator-criteria.section.tsx +83 -0
  91. package/src/components/indicators/base-indicator-details-form.component.tsx +80 -0
  92. package/src/components/indicators/base-indicator-theme-unit-fields.component.tsx +39 -0
  93. package/src/components/indicators/create-base-indicator-modal.component.tsx +501 -0
  94. package/src/components/indicators/create-composite-base-indicator-modal.component.tsx +297 -0
  95. package/src/components/indicators/create-final-indicator-modal.component.tsx +260 -0
  96. package/src/components/indicators/diagnosis-filters-form.component.tsx +101 -0
  97. package/src/components/indicators/disaggregation-panel.component.tsx +93 -0
  98. package/src/components/indicators/handler/concept-search-multiselect.component.tsx +153 -0
  99. package/src/components/indicators/handler/question-answer-concept-search.component.tsx +336 -0
  100. package/src/components/indicators/indicator-status-tag.component.tsx +9 -0
  101. package/src/components/indicators/indicators-page.component.tsx +481 -0
  102. package/src/components/indicators/indicators-page.scss +121 -0
  103. package/src/components/indicators/indicators-table.component.tsx +166 -0
  104. package/src/components/indicators/preview-table.component.tsx +31 -0
  105. package/src/components/indicators/sections/composite-indicator-basics.section.tsx +36 -0
  106. package/src/components/indicators/sections/composite-indicator-picker.section.tsx +161 -0
  107. package/src/components/indicators/sections/composite-indicator-sql-preview.section.tsx +31 -0
  108. package/src/components/indicators/sections/final-indicator-basics.section.tsx +46 -0
  109. package/src/components/indicators/sections/final-indicator-disaggregation.section.tsx +47 -0
  110. package/src/components/indicators/sections/final-indicator-picker.section.tsx +92 -0
  111. package/src/components/indicators/sections/final-indicator-preview.section.tsx +20 -0
  112. package/src/components/indicators/sections/final-indicator-results-preview.section.tsx +65 -0
  113. package/src/components/indicators/sections/indicator-basics.section.tsx +40 -0
  114. package/src/components/indicators/sections/indicator-conditions.section.tsx +194 -0
  115. package/src/components/indicators/sections/indicator-sql-preview.section.tsx +26 -0
  116. package/src/components/indicators/sections/indicator-theme.section.tsx +48 -0
  117. package/src/components/indicators/sql-preview.component.tsx +19 -0
  118. package/src/components/indicators/types/base-indicator-form.types.ts +17 -0
  119. package/src/components/indicators/types/base-indicator-theme.constants.ts +8 -0
  120. package/src/components/indicators/types/composite-indicator.types.ts +21 -0
  121. package/src/components/indicators/types/condition-ui.types.ts +11 -0
  122. package/src/components/indicators/types/data-theme-config.types.ts +88 -0
  123. package/src/components/indicators/types/indicator-types.ts +56 -0
  124. package/src/components/indicators/types/sql-builders.ts +43 -0
  125. package/src/components/indicators/utils/composite-indicator-sql.utils.ts +206 -0
  126. package/src/components/indicators/utils/concept-preload.utils.ts +21 -0
  127. package/src/components/indicators/utils/final-indicator-sql.utils.ts +124 -0
  128. package/src/components/indicators/utils/indicator-conditions-hydration.utils.ts +254 -0
  129. package/src/components/indicators/utils/indicator-sql.utils.ts +185 -0
  130. package/src/components/landing/landing-action-item.component.tsx +50 -0
  131. package/src/components/landing/landing-action-list.component.tsx +12 -0
  132. package/src/components/landing/landing-activity-list.component.tsx +47 -0
  133. package/src/components/landing/landing-footer-grid.component.tsx +45 -0
  134. package/src/components/landing/landing-hero.component.tsx +18 -0
  135. package/src/components/landing/landing-section-card.component.tsx +33 -0
  136. package/src/components/landing/report-builder-landing-page.component.tsx +103 -0
  137. package/src/components/landing/report-builder-landing-page.scss +131 -0
  138. package/src/components/report/dashboard/reports-table.component.tsx +145 -0
  139. package/src/components/report/definition/panels/properties-panel.component.tsx +38 -0
  140. package/src/components/report/definition/panels/report-structure-panel.component.tsx +61 -0
  141. package/src/components/report/definition/panels/selection-panel.component.tsx +58 -0
  142. package/src/components/report/definition/report-definition-editor.component.tsx +99 -0
  143. package/src/components/report/definition/report-definition.types.ts +66 -0
  144. package/src/components/report/design/panels/IndicatorPropertiesPanel.scss +33 -0
  145. package/src/components/report/design/panels/IndicatorPropertiesPanel.tsx +389 -0
  146. package/src/components/report/design/panels/JsonPreviewPanel.scss +32 -0
  147. package/src/components/report/design/panels/JsonPreviewPanel.tsx +104 -0
  148. package/src/components/report/design/panels/mapping-preview-panel.component.tsx +82 -0
  149. package/src/components/report/design/panels/mapping-preview-panel.scss +55 -0
  150. package/src/components/report/design/panels/template-structure-panel.component.tsx +296 -0
  151. package/src/components/report/design/panels/template-structure-panel.scss +81 -0
  152. package/src/components/report/design/report-design-editor.component.tsx +894 -0
  153. package/src/components/report/design/report-design.types.ts +43 -0
  154. package/src/components/report/design/report-design.utils.ts +86 -0
  155. package/src/components/report/panels/report-detail.component.tsx +71 -0
  156. package/src/components/report/panels/report-panel-shell.component.tsx +60 -0
  157. package/src/components/report/report-dashboard.page.component.tsx +75 -0
  158. package/src/components/report/report-editor.page.component.tsx +466 -0
  159. package/src/components/report/report.routes.tsx +30 -0
  160. package/src/components/report-sections/create-report-section-modal.component.tsx +278 -0
  161. package/src/components/report-sections/hooks/useAgeCategories.ts +25 -0
  162. package/src/components/report-sections/hooks/useSectionEditorState.ts +306 -0
  163. package/src/components/report-sections/panels/Dhis2MappingPanel.tsx +157 -0
  164. package/src/components/report-sections/panels/DisaggregationPanel.tsx +97 -0
  165. package/src/components/report-sections/panels/IndicatorPickerPanel.tsx +143 -0
  166. package/src/components/report-sections/panels/SectionBasicsPanel.tsx +27 -0
  167. package/src/components/report-sections/report-section-preview-modal.component.tsx +418 -0
  168. package/src/components/report-sections/report-sections-page.component.tsx +313 -0
  169. package/src/components/report-sections/section-types.ts +55 -0
  170. package/src/components/report-sections/section-utils.ts +46 -0
  171. package/src/components/report-sections/sections-page.scss +79 -0
  172. package/src/components/run-reports/run-reports-page.component.tsx +23 -0
  173. package/src/components/shared/header/builder-header-actions.component.tsx +38 -0
  174. package/src/components/shared/header/header.component.tsx +85 -0
  175. package/src/components/shared/header/header.scss +150 -0
  176. package/src/components/shared/header/illustration.component.tsx +34 -0
  177. package/src/components/shared/indicator-properties-panel/IndicatorPropertiesPanel.scss +33 -0
  178. package/src/components/shared/indicator-properties-panel/IndicatorPropertiesPanel.tsx +389 -0
  179. package/src/components/shared/json-preview-panel/JsonPreviewPanel.scss +32 -0
  180. package/src/components/shared/json-preview-panel/JsonPreviewPanel.tsx +104 -0
  181. package/src/components/shared/mapping-preview-panel/mapping-preview-panel.component.tsx +82 -0
  182. package/src/components/shared/mapping-preview-panel/mapping-preview-panel.scss +55 -0
  183. package/src/components/shared/preview/query-results-preview.component.tsx +204 -0
  184. package/src/components/shared/template-structure-panel/template-structure-panel.component.tsx +296 -0
  185. package/src/components/shared/template-structure-panel/template-structure-panel.scss +81 -0
  186. package/src/config-schema.ts +23 -0
  187. package/src/globals.d.ts +14 -0
  188. package/src/hooks/theme/useDataThemes.ts +28 -0
  189. package/src/hooks/theme/useMambaTableMeta.ts +34 -0
  190. package/src/hooks/theme/useMambaTables.ts +63 -0
  191. package/src/index.ts +23 -0
  192. package/src/resources/agegroup/agegroups.api.ts +58 -0
  193. package/src/resources/concepts/concept-mapper.ts +21 -0
  194. package/src/resources/concepts/concept-types.ts +31 -0
  195. package/src/resources/concepts/concepts.resource.ts +44 -0
  196. package/src/resources/concepts/useConceptSearch.ts +41 -0
  197. package/src/resources/etl-source/etl-source.api.ts +65 -0
  198. package/src/resources/indicator/indicators.api.ts +125 -0
  199. package/src/resources/openmrs-api.ts +24 -0
  200. package/src/resources/preview/sql-preview.api.ts +54 -0
  201. package/src/resources/report/reports.api.ts +117 -0
  202. package/src/resources/report-category/report-category.api.ts +41 -0
  203. package/src/resources/report-library/report-library.api.ts +63 -0
  204. package/src/resources/report-section/report-sections.api.ts +73 -0
  205. package/src/resources/report-section/section-preview.api.ts +21 -0
  206. package/src/resources/theme/data-theme.api.ts +88 -0
  207. package/src/resources/theme/mamba-schema.api.ts +31 -0
  208. package/src/resources/theme/mamba-table-meta.api.ts +45 -0
  209. package/src/root.component.tsx +20 -0
  210. package/src/routes/report-builder.routes.tsx +66 -0
  211. package/src/routes/report-builder.scss +56 -0
  212. package/src/routes.json +21 -0
  213. package/src/sample-template.ts +121 -0
  214. package/src/template-utils.ts +110 -0
  215. package/src/types/carbon-react-inert.d.ts +13 -0
  216. package/src/types/condition-operators.ts +49 -0
  217. package/src/types/theme/data-theme.types.ts +88 -0
  218. package/src/utils/html-entities.utils.ts +23 -0
  219. package/tools/setup-tests.ts +1 -0
  220. package/translations/en.json +4 -0
  221. package/tsconfig.json +43 -0
  222. package/webpack.config.js +14 -0
@@ -0,0 +1,104 @@
1
+ import React, { useMemo, useState } from 'react';
2
+ import { Button, CopyButton, IconButton } from '@carbon/react';
3
+ import { Download, Maximize, Minimize, Renew } from '@carbon/icons-react';
4
+ import AceEditor from 'react-ace';
5
+
6
+ import 'ace-builds/src-noconflict/mode-json';
7
+ import 'ace-builds/src-noconflict/theme-github';
8
+
9
+ import styles from './JsonPreviewPanel.scss';
10
+
11
+ type Props = {
12
+ /** Draft JSON shown in the editor (controlled) */
13
+ jsonString: string;
14
+
15
+ /** Called when user edits the JSON */
16
+ onChangeJson?: (value: string) => void;
17
+
18
+ /** Refresh Template Structure from JSON draft */
19
+ onRefreshTemplate?: () => void;
20
+
21
+ fileName?: string;
22
+
23
+ /** Defaults to true to preserve current behaviour */
24
+ readOnly?: boolean;
25
+ };
26
+
27
+ export default function JsonPreviewPanel({
28
+ jsonString,
29
+ onChangeJson,
30
+ onRefreshTemplate,
31
+ fileName = 'report-template.json',
32
+ readOnly = true,
33
+ }: Props) {
34
+ const [isMaximized, setIsMaximized] = useState(false);
35
+
36
+ const downloadable = useMemo(() => new Blob([jsonString], { type: 'application/json' }), [jsonString]);
37
+ const downloadUrl = useMemo(() => window.URL.createObjectURL(downloadable), [downloadable]);
38
+
39
+ const canEdit = !readOnly && typeof onChangeJson === 'function';
40
+ const canRefreshTemplate = typeof onRefreshTemplate === 'function';
41
+
42
+ return (
43
+ <div className={isMaximized ? styles.maximized : ''}>
44
+ <div className={styles.headerRow}>
45
+ <h3 className={styles.title}>JSON Preview</h3>
46
+
47
+ <div className={styles.tools}>
48
+ {/* ✅ Refresh Template (JSON -> Template Structure) */}
49
+ {canRefreshTemplate ? (
50
+ <Button
51
+ size="sm"
52
+ kind="ghost"
53
+ renderIcon={Renew}
54
+ onClick={onRefreshTemplate}
55
+ className={styles.refreshBtn}
56
+ />
57
+ ) : null}
58
+
59
+ <CopyButton
60
+ align="top"
61
+ className="cds--btn--sm"
62
+ iconDescription="Copy JSON"
63
+ kind="ghost"
64
+ onClick={() => navigator.clipboard.writeText(jsonString)}
65
+ />
66
+
67
+ <a download={fileName} href={downloadUrl}>
68
+ <IconButton kind="ghost" label="Download JSON" size="sm">
69
+ <Download />
70
+ </IconButton>
71
+ </a>
72
+
73
+ <IconButton
74
+ kind="ghost"
75
+ label={isMaximized ? 'Minimize' : 'Maximize'}
76
+ size="sm"
77
+ onClick={() => setIsMaximized((v) => !v)}
78
+ >
79
+ {isMaximized ? <Minimize /> : <Maximize />}
80
+ </IconButton>
81
+ </div>
82
+ </div>
83
+
84
+ <div className={styles.editorContainer}>
85
+ <AceEditor
86
+ mode="json"
87
+ theme="github"
88
+ value={jsonString}
89
+ readOnly={!canEdit}
90
+ onChange={(value) => onChangeJson?.(value)}
91
+ width="100%"
92
+ height={isMaximized ? '70vh' : '320px'}
93
+ fontSize={12}
94
+ setOptions={{
95
+ useWorker: false,
96
+ showPrintMargin: false,
97
+ tabSize: 2,
98
+ wrap: true,
99
+ }}
100
+ />
101
+ </div>
102
+ </div>
103
+ );
104
+ }
@@ -0,0 +1,82 @@
1
+ import React from 'react';
2
+ import { Tile, Toggle } from '@carbon/react';
3
+ import { useTranslation } from 'react-i18next';
4
+
5
+ import styles from './mapping-preview-panel.scss';
6
+
7
+ type Props = {
8
+ title?: string;
9
+
10
+ // Controlled toggle state
11
+ zerosMode: boolean;
12
+ onToggleZerosMode: (value: boolean) => void;
13
+
14
+ // Row label (selected indicator)
15
+ rowLabel?: string;
16
+
17
+ // Column headers (disaggregation preview)
18
+ headers?: string[];
19
+
20
+ // Sample values (same length as headers)
21
+ sampleValues?: number[];
22
+
23
+ // Optional: allow parent to pass className for sizing
24
+ className?: string;
25
+ };
26
+
27
+ export default function MappingPreviewPanel({
28
+ title,
29
+ zerosMode,
30
+ onToggleZerosMode,
31
+ rowLabel,
32
+ headers = ['Age <5 | Male', 'Age <5 | Female', '5-14 | Male', '5-14 | Female'],
33
+ sampleValues = [120, 80, 75, 60],
34
+ className,
35
+ }: Props) {
36
+ const { t } = useTranslation();
37
+
38
+ const safeValues = headers.map((_, i) => sampleValues[i] ?? 0);
39
+
40
+ return (
41
+ <Tile className={`${styles.tile} ${className ?? ''}`}>
42
+ <div className={styles.tileHeaderRow}>
43
+ <h3 className={styles.tileTitle}>{title ?? t('mappingPreview', 'Mapping Preview')}</h3>
44
+
45
+ <div className={styles.previewControls}>
46
+ <Toggle
47
+ id="zerosMode"
48
+ labelText=""
49
+ hideLabel
50
+ labelA={t('sampleValues', 'Sample Values')}
51
+ labelB={t('zerosMode', 'Zeros Mode')}
52
+ toggled={zerosMode}
53
+ onToggle={onToggleZerosMode}
54
+ size="sm"
55
+ />
56
+ </div>
57
+ </div>
58
+
59
+ <div className={styles.previewTableWrap}>
60
+ <table className={styles.previewTable}>
61
+ <thead>
62
+ <tr>
63
+ <th />
64
+ {headers.map((h) => (
65
+ <th key={h}>{h}</th>
66
+ ))}
67
+ </tr>
68
+ </thead>
69
+
70
+ <tbody>
71
+ <tr>
72
+ <td className={styles.previewRowLabel}>{rowLabel ?? 'EP01A. Malaria Cases'}</td>
73
+ {safeValues.map((v, i) => (
74
+ <td key={i}>{zerosMode ? 0 : v}</td>
75
+ ))}
76
+ </tr>
77
+ </tbody>
78
+ </table>
79
+ </div>
80
+ </Tile>
81
+ );
82
+ }
@@ -0,0 +1,55 @@
1
+ .tile {
2
+ padding: 1rem;
3
+ border-radius: 0.5rem;
4
+ display: flex;
5
+ flex-direction: column;
6
+ min-height: 0;
7
+ }
8
+
9
+ .tileHeaderRow {
10
+ display: flex;
11
+ align-items: center;
12
+ justify-content: space-between;
13
+ gap: 0.75rem;
14
+ margin-bottom: 0.75rem;
15
+ }
16
+
17
+ .tileTitle {
18
+ margin: 0;
19
+ font-weight: 600;
20
+ }
21
+
22
+ .previewControls {
23
+ display: flex;
24
+ align-items: center;
25
+ gap: 0.75rem;
26
+ }
27
+
28
+ .previewTableWrap {
29
+ border: 1px solid var(--cds-border-subtle, #e0e0e0);
30
+ border-radius: 0.375rem;
31
+ overflow: hidden;
32
+ }
33
+
34
+ .previewTable {
35
+ width: 100%;
36
+ border-collapse: collapse;
37
+ }
38
+
39
+ .previewTable thead th {
40
+ background: var(--cds-layer-02, #f4f4f4);
41
+ font-weight: 600;
42
+ text-align: left;
43
+ padding: 0.75rem;
44
+ border-bottom: 1px solid var(--cds-border-subtle, #e0e0e0);
45
+ }
46
+
47
+ .previewTable tbody td {
48
+ padding: 0.75rem;
49
+ border-bottom: 1px solid var(--cds-border-subtle, #e0e0e0);
50
+ }
51
+
52
+ .previewRowLabel {
53
+ font-weight: 600;
54
+ white-space: nowrap;
55
+ }
@@ -0,0 +1,204 @@
1
+ import React from 'react';
2
+ import {
3
+ Button,
4
+ InlineLoading,
5
+ InlineNotification,
6
+ DataTable,
7
+ Table,
8
+ TableBody,
9
+ TableCell,
10
+ TableContainer,
11
+ TableHead,
12
+ TableHeader,
13
+ TableRow,
14
+ CodeSnippet,
15
+ Accordion,
16
+ AccordionItem,
17
+ Tag,
18
+ } from '@carbon/react';
19
+ import { Play } from '@carbon/icons-react';
20
+
21
+ import { previewSql, type SqlPreviewResponse } from '../../../resources/preview/sql-preview.api';
22
+
23
+ type Props = {
24
+ title?: string;
25
+
26
+ sql: string;
27
+ params?: Record<string, any>;
28
+ maxRows?: number;
29
+
30
+ /**
31
+ * If false, user cannot run preview (e.g. missing dates)
32
+ */
33
+ canRun?: boolean;
34
+
35
+ /**
36
+ * Auto-run when inputs change (default false)
37
+ */
38
+ autoRun?: boolean;
39
+ };
40
+
41
+ export default function QueryResultsPreview({
42
+ title = 'Results preview',
43
+ sql,
44
+ params,
45
+ maxRows = 200,
46
+ canRun = true,
47
+ autoRun = false,
48
+ }: Props) {
49
+ const [loading, setLoading] = React.useState(false);
50
+ const [err, setErr] = React.useState<string | null>(null);
51
+ const [data, setData] = React.useState<SqlPreviewResponse | null>(null);
52
+
53
+ const run = React.useCallback(async () => {
54
+ if (!canRun) return;
55
+ if (!sql?.trim()) return;
56
+
57
+ const ac = new AbortController();
58
+ setLoading(true);
59
+ setErr(null);
60
+
61
+ try {
62
+ const resp = await previewSql({ sql, params, maxRows }, ac.signal);
63
+ setData(resp);
64
+ } catch (e: any) {
65
+ setErr(e?.message ?? 'Failed to preview query');
66
+ setData(null);
67
+ } finally {
68
+ setLoading(false);
69
+ }
70
+
71
+ return () => ac.abort();
72
+ }, [sql, params, maxRows, canRun]);
73
+
74
+ // optional autorun
75
+ React.useEffect(() => {
76
+ if (!autoRun) return;
77
+ if (!canRun) return;
78
+ if (!sql?.trim()) return;
79
+ run();
80
+ // eslint-disable-next-line react-hooks/exhaustive-deps
81
+ }, [autoRun, canRun, sql, JSON.stringify(params ?? {}), maxRows]);
82
+
83
+ const columns = data?.columns ?? [];
84
+ const rows = data?.rows ?? [];
85
+
86
+ const headers = React.useMemo(() => columns.map((c) => ({ key: c, header: c })), [columns]);
87
+
88
+ const tableRows = React.useMemo(() => {
89
+ return rows.map((r, idx) => {
90
+ const obj: any = { id: String(idx) };
91
+ columns.forEach((c, i) => {
92
+ obj[c] = r[i];
93
+ });
94
+ return obj;
95
+ });
96
+ }, [rows, columns]);
97
+
98
+ const hasSql = Boolean(sql?.trim());
99
+ const canClickRun = Boolean(canRun && hasSql && !loading);
100
+
101
+ return (
102
+ <div
103
+ style={{
104
+ display: 'grid',
105
+ gap: '0.75rem',
106
+ padding: '1rem',
107
+ borderRadius: '0.5rem',
108
+ background: 'var(--cds-layer, #ffffff)',
109
+ border: '1px solid var(--cds-border-subtle, #e0e0e0)',
110
+ }}
111
+ >
112
+ {/* Header */}
113
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: '1rem' }}>
114
+ <div style={{ display: 'grid', gap: '0.25rem' }}>
115
+ <div style={{ fontWeight: 600 }}>{title}</div>
116
+ <div style={{ fontSize: '0.875rem', opacity: 0.75 }}>
117
+ {canRun ? 'Ready to preview results.' : 'Select required parameters to enable preview.'}
118
+ </div>
119
+ </div>
120
+
121
+ <Button kind="secondary" size="sm" renderIcon={Play} disabled={!canClickRun} onClick={() => run()}>
122
+ Run preview
123
+ </Button>
124
+ </div>
125
+
126
+ {/* SQL (collapsible) */}
127
+ <Accordion align="start">
128
+ <AccordionItem title="SQL (click to expand)">
129
+ <CodeSnippet type="multi" wrapText>
130
+ {hasSql ? sql : '-- No SQL to preview yet'}
131
+ </CodeSnippet>
132
+ </AccordionItem>
133
+ </Accordion>
134
+
135
+ {/* Status row */}
136
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '1rem' }}>
137
+ <div>
138
+ {loading ? <InlineLoading description="Running preview…" /> : null}
139
+ {!loading && err ? (
140
+ <InlineNotification kind="error" lowContrast title="Preview error" subtitle={err} />
141
+ ) : null}
142
+ </div>
143
+
144
+ {data && !loading && !err ? (
145
+ <div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center', flexWrap: 'wrap' }}>
146
+ <Tag type="cool-gray" size="sm">
147
+ Rows: {data.rowCount}
148
+ </Tag>
149
+ {data.truncated ? (
150
+ <Tag type="red" size="sm">
151
+ Truncated to {maxRows}
152
+ </Tag>
153
+ ) : (
154
+ <Tag type="green" size="sm">
155
+ Not truncated
156
+ </Tag>
157
+ )}
158
+ </div>
159
+ ) : null}
160
+ </div>
161
+
162
+ {/* Results table */}
163
+ {data && !loading && !err ? (
164
+ <div
165
+ style={{
166
+ border: '1px solid var(--cds-border-subtle, #e0e0e0)',
167
+ borderRadius: '0.25rem',
168
+ overflow: 'hidden',
169
+ background: 'var(--cds-layer, #ffffff)',
170
+ }}
171
+ >
172
+ <div style={{ maxHeight: '320px', overflow: 'auto' }}>
173
+ <DataTable rows={tableRows} headers={headers} size="sm">
174
+ {({ rows, headers, getHeaderProps, getRowProps }) => (
175
+ <TableContainer>
176
+ <Table size="sm" useZebraStyles>
177
+ <TableHead>
178
+ <TableRow>
179
+ {headers.map((h) => (
180
+ <TableHeader key={h.key} {...getHeaderProps({ header: h })}>
181
+ {h.header}
182
+ </TableHeader>
183
+ ))}
184
+ </TableRow>
185
+ </TableHead>
186
+ <TableBody>
187
+ {rows.map((row) => (
188
+ <TableRow key={row.id} {...getRowProps({ row })}>
189
+ {row.cells.map((cell) => (
190
+ <TableCell key={cell.id}>{String(cell.value ?? '')}</TableCell>
191
+ ))}
192
+ </TableRow>
193
+ ))}
194
+ </TableBody>
195
+ </Table>
196
+ </TableContainer>
197
+ )}
198
+ </DataTable>
199
+ </div>
200
+ </div>
201
+ ) : null}
202
+ </div>
203
+ );
204
+ }