diff --git a/web/src/api/asset_interfaces/metric/interfaces.ts b/web/src/api/asset_interfaces/metric/interfaces.ts
index ec6a33940..47b61f71d 100644
--- a/web/src/api/asset_interfaces/metric/interfaces.ts
+++ b/web/src/api/asset_interfaces/metric/interfaces.ts
@@ -7,6 +7,7 @@ export type BusterMetric = {
title: string;
version_number: number;
description: string | null;
+ file_name: string;
time_frame: string;
dataset_id: string;
data_source_id: string;
diff --git a/web/src/app/app/_components/Popups/SaveResetFilePopup.tsx b/web/src/app/app/_components/Popups/SaveResetFilePopup.tsx
new file mode 100644
index 000000000..1158dcb8b
--- /dev/null
+++ b/web/src/app/app/_components/Popups/SaveResetFilePopup.tsx
@@ -0,0 +1,53 @@
+import { PopupContainer, PopupSplitter } from '@/components/popup';
+import React from 'react';
+import { Text } from '@/components/text';
+import { Button } from 'antd';
+import { AppMaterialIcons } from '@/components';
+import { createStyles } from 'antd-style';
+
+export const SaveResetFilePopup: React.FC<{
+ open: boolean;
+ onReset: () => void;
+ onSave: () => void;
+}> = React.memo(({ open, onReset, onSave }) => {
+ return (
+
+
+
+ );
+});
+
+const SplitterContent: React.FC<{
+ onReset: () => void;
+ onSave: () => void;
+}> = React.memo(({ onReset, onSave }) => {
+ const { styles, cx } = useStyles();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+});
+
+SaveResetFilePopup.displayName = 'SaveResetFilePopup';
+SplitterContent.displayName = 'SplitterContent';
+const useStyles = createStyles(({ css, token }) => ({
+ icon: css`
+ color: ${token.colorIcon};
+ `
+}));
diff --git a/web/src/app/app/_controllers/MetricController/MetricViewFile/MetricViewFile.tsx b/web/src/app/app/_controllers/MetricController/MetricViewFile/MetricViewFile.tsx
index 57a4ba48e..9f7c7720a 100644
--- a/web/src/app/app/_controllers/MetricController/MetricViewFile/MetricViewFile.tsx
+++ b/web/src/app/app/_controllers/MetricController/MetricViewFile/MetricViewFile.tsx
@@ -1,8 +1,44 @@
-import React from 'react';
+import React, { useEffect } from 'react';
import type { MetricViewProps } from '../config';
+import { CodeCard } from '@/components/card';
+import { useBusterMetricIndividual, useBusterMetricsContextSelector } from '@/context/Metrics';
+import { useMemoizedFn } from 'ahooks';
+import { SaveResetFilePopup } from '@appComponents/Popups/SaveResetFilePopup';
+import { useBusterNotifications } from '@/context/BusterNotifications';
export const MetricViewFile: React.FC = React.memo(({ metricId }) => {
- return MetricViewFile
;
+ const { metric } = useBusterMetricIndividual({ metricId });
+ const { openSuccessMessage } = useBusterNotifications();
+ const onUpdateMetric = useBusterMetricsContextSelector((x) => x.onUpdateMetric);
+
+ const { file: fileProp, file_name } = metric;
+
+ const [file, setFile] = React.useState(fileProp);
+
+ const showPopup = file !== fileProp && !!file;
+
+ const onResetFile = useMemoizedFn(() => {
+ setFile(fileProp);
+ });
+
+ const onSaveFile = useMemoizedFn(async () => {
+ await onUpdateMetric({
+ file
+ });
+ openSuccessMessage(`${file_name} saved`);
+ });
+
+ useEffect(() => {
+ setFile(fileProp);
+ }, [fileProp]);
+
+ return (
+
+
+
+
+
+ );
});
MetricViewFile.displayName = 'MetricViewFile';
diff --git a/web/src/components/card/CodeCard.tsx b/web/src/components/card/CodeCard.tsx
new file mode 100644
index 000000000..e46bab9ef
--- /dev/null
+++ b/web/src/components/card/CodeCard.tsx
@@ -0,0 +1,120 @@
+import { createStyles } from 'antd-style';
+import React from 'react';
+import { Text } from '@/components/text';
+import { AppCodeEditor } from '../inputs/AppCodeEditor';
+import { Button } from 'antd';
+import { AppMaterialIcons } from '../icons';
+import { useMemoizedFn } from 'ahooks';
+import { useBusterNotifications } from '@/context/BusterNotifications';
+
+export const CodeCard: React.FC<{
+ code: string;
+ language: string;
+ fileName: string;
+ className?: string;
+ bodyClassName?: string;
+ buttons?: React.ReactNode | true;
+ onChange?: (code: string) => void;
+ readOnly?: boolean;
+}> = ({
+ code,
+ language = 'yml',
+ fileName,
+ className = 'h-full overflow-hidden',
+ bodyClassName = 'h-full',
+ buttons = true,
+ onChange,
+ readOnly = false
+}) => {
+ const { styles, cx } = useStyles();
+
+ const ShownButtons = buttons === true ? : buttons;
+
+ return (
+
+
+ {fileName}
+
+ {ShownButtons}
+
+
+
+ );
+};
+
+const CardButtons: React.FC<{
+ fileName: string;
+ code: string;
+}> = React.memo(({ fileName, code }) => {
+ const { openInfoMessage, openErrorMessage } = useBusterNotifications();
+
+ const handleCopyCode = useMemoizedFn(async () => {
+ try {
+ await navigator.clipboard.writeText(code);
+ openInfoMessage('Code copied to clipboard');
+ } catch (error) {
+ openErrorMessage('Failed to copy code');
+ }
+ });
+
+ const handleDownload = useMemoizedFn(() => {
+ try {
+ const blob = new Blob([code], { type: 'text/plain' });
+ const url = window.URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = fileName;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ window.URL.revokeObjectURL(url);
+ } catch (error) {
+ openErrorMessage('Failed to download file');
+ }
+ });
+
+ return (
+
+ }
+ onClick={handleCopyCode}
+ title="Copy code"
+ />
+ }
+ onClick={handleDownload}
+ title="Download file"
+ />
+
+ );
+});
+CardButtons.displayName = 'CardButtons';
+
+const useStyles = createStyles(({ token, css }) => ({
+ container: css`
+ border-radius: ${token.borderRadius}px;
+ border: 0.5px solid ${token.colorBorder};
+ `,
+ containerHeader: css`
+ background: ${token.controlItemBgActive};
+ border-bottom: 0.5px solid ${token.colorBorder};
+ height: 32px;
+ `,
+ containerBody: css`
+ background: ${token.colorBgBase};
+ `
+}));
diff --git a/web/src/components/card/index.ts b/web/src/components/card/index.ts
index e655c9d50..fe94d19b8 100644
--- a/web/src/components/card/index.ts
+++ b/web/src/components/card/index.ts
@@ -1 +1,2 @@
export * from './ItemContainer';
+export * from './CodeCard';
diff --git a/web/src/components/list/BusterList/BusterListSelectedOptionPopup/BusterListSelectedOptionPopupContainer.tsx b/web/src/components/list/BusterList/BusterListSelectedOptionPopup/BusterListSelectedOptionPopupContainer.tsx
index f9f5a4dc8..1a13d4f5f 100644
--- a/web/src/components/list/BusterList/BusterListSelectedOptionPopup/BusterListSelectedOptionPopupContainer.tsx
+++ b/web/src/components/list/BusterList/BusterListSelectedOptionPopup/BusterListSelectedOptionPopupContainer.tsx
@@ -4,6 +4,7 @@ import { useAntToken } from '@/styles/useAntToken';
import { createStyles } from 'antd-style';
import React from 'react';
import { AnimatePresence, motion } from 'framer-motion';
+import { PopupContainer, PopupSplitter } from '@/components/popup/PopupContainer';
export const BusterListSelectedOptionPopupContainer: React.FC<{
selectedRowKeys: string[];
@@ -16,44 +17,17 @@ export const BusterListSelectedOptionPopupContainer: React.FC<{
const show = showProp ?? selectedRowKeys.length > 0;
return (
-
- {show && (
-
-
- {show && (
-
-
+
+
+
- {buttons.length > 0 && (
-
- )}
+ {buttons.length > 0 &&
}
- {buttons.map((button, index) => (
-
{button}
- ))}
-
- )}
-
-
- )}
-
+ {buttons.map((button, index) => (
+
{button}
+ ))}
+
+
);
};
diff --git a/web/src/components/popup/PopupContainer.tsx b/web/src/components/popup/PopupContainer.tsx
new file mode 100644
index 000000000..3a26bc955
--- /dev/null
+++ b/web/src/components/popup/PopupContainer.tsx
@@ -0,0 +1,48 @@
+import React from 'react';
+import { AnimatePresence, motion } from 'framer-motion';
+import { createStyles } from 'antd-style';
+
+export const PopupContainer: React.FC<{
+ show: boolean;
+ children: React.ReactNode;
+ secondaryChildren?: React.ReactNode;
+}> = ({ show, children, secondaryChildren }) => {
+ const { styles, cx } = useStyles();
+
+ return (
+
+ {show && (
+
+ {show && <>{children}>}
+
+ )}
+
+ );
+};
+
+export const PopupSplitter: React.FC<{}> = ({}) => {
+ const { styles, cx } = useStyles();
+ return ;
+};
+
+const useStyles = createStyles(({ css, token }) => ({
+ container: css`
+ background-color: ${token.colorBgContainer};
+ border-radius: ${token.borderRadius}px;
+ box-shadow: ${token.boxShadowSecondary};
+ padding: ${token.paddingXS}px;
+ `,
+ splitter: css`
+ background-color: ${token.colorSplit};
+ height: ${token.controlHeight - 7}px;
+ width: 0.5px;
+ `
+}));
diff --git a/web/src/components/popup/index.ts b/web/src/components/popup/index.ts
new file mode 100644
index 000000000..5000baded
--- /dev/null
+++ b/web/src/components/popup/index.ts
@@ -0,0 +1 @@
+export * from './PopupContainer';
diff --git a/web/src/context/Metrics/MOCK_METRIC.ts b/web/src/context/Metrics/MOCK_METRIC.ts
index 37e2db0ab..bf370db63 100644
--- a/web/src/context/Metrics/MOCK_METRIC.ts
+++ b/web/src/context/Metrics/MOCK_METRIC.ts
@@ -62,6 +62,7 @@ export const MOCK_METRIC: IBusterMetric = {
id: '123',
title: 'Mock Metric',
version_number: 1,
+ file_name: 'mock_metric.yml',
description: faker.lorem.sentence(33),
data_source_id: '6840fa04-c0d7-4e0e-8d3d-ea9190d93874',
time_frame: '1d',
@@ -77,7 +78,48 @@ export const MOCK_METRIC: IBusterMetric = {
status: VerificationStatus.notRequested,
evaluation_score: 'Moderate',
evaluation_summary: faker.lorem.sentence(33),
- file: '',
+ file: `
+metric:
+ name: sales_performance
+ description: Monthly sales performance by product
+ source: sales_database
+ refresh_interval: daily
+
+dimensions:
+ - name: date
+ type: date
+ format: YYYY-MM-DD
+ - name: product
+ type: string
+ - name: region
+ type: string
+
+measures:
+ - name: sales_amount
+ type: decimal
+ aggregation: sum
+ - name: units_sold
+ type: integer
+ aggregation: sum
+
+filters:
+ - field: date
+ operator: between
+ value: [2024-01-01, 2024-12-31]
+ - field: region
+ operator: in
+ value: [North, South, East, West]
+
+joins:
+ - name: product_details
+ type: left
+ on: product_id
+
+sorting:
+ - field: sales_amount
+ direction: desc
+
+limit: 1000`,
created_at: '',
updated_at: '',
sent_by_id: '',
diff --git a/web/src/context/Metrics/config.ts b/web/src/context/Metrics/config.ts
index 9f8815176..56b22f366 100644
--- a/web/src/context/Metrics/config.ts
+++ b/web/src/context/Metrics/config.ts
@@ -16,14 +16,16 @@ export const defaultIBusterMetric: Required = {
fetchedAt: 0,
code: null,
feedback: null,
- dataset_id: null,
+ dataset_id: '',
dataset_name: null,
error: null,
data_metadata: null,
status: VerificationStatus.notRequested,
evaluation_score: 'Moderate',
evaluation_summary: '',
+ file_name: '',
file: '',
+ data_source_id: '',
created_at: '',
updated_at: '',
sent_by_id: '',
diff --git a/web/src/context/Metrics/helpers/saveToServerHelpers.ts b/web/src/context/Metrics/helpers/saveToServerHelpers.ts
index ac0625878..d32690822 100644
--- a/web/src/context/Metrics/helpers/saveToServerHelpers.ts
+++ b/web/src/context/Metrics/helpers/saveToServerHelpers.ts
@@ -22,7 +22,7 @@ const DEFAULT_COLUMN_SETTINGS_ENTRIES = Object.entries(DEFAULT_COLUMN_SETTINGS);
const DEFAULT_COLUMN_LABEL_FORMATS_ENTRIES = Object.entries(DEFAULT_COLUMN_LABEL_FORMAT);
const getChangedTopLevelMessageValues = (newMetric: IBusterMetric, oldMetric: IBusterMetric) => {
- return getChangedValues(oldMetric, newMetric, ['title', 'feedback', 'status', 'code']);
+ return getChangedValues(oldMetric, newMetric, ['title', 'feedback', 'status', 'code', 'file']);
};
const keySpecificHandlers: Partial any>> = {
diff --git a/web/src/context/SQL/useSQLProvider.tsx b/web/src/context/SQL/useSQLProvider.tsx
index 5b2f87075..57438a0c1 100644
--- a/web/src/context/SQL/useSQLProvider.tsx
+++ b/web/src/context/SQL/useSQLProvider.tsx
@@ -100,7 +100,7 @@ export const useSQLProvider = () => {
return result;
} catch (error) {
- console.log(error);
+ //
}
}
);