popup selet file container

This commit is contained in:
Nate Kelley 2025-02-05 14:25:55 -07:00
parent 2fc8c9a221
commit 1a373d1e04
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
12 changed files with 320 additions and 42 deletions

View File

@ -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;

View File

@ -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 (
<PopupContainer show={open}>
<SplitterContent onReset={onReset} onSave={onSave} />
</PopupContainer>
);
});
const SplitterContent: React.FC<{
onReset: () => void;
onSave: () => void;
}> = React.memo(({ onReset, onSave }) => {
const { styles, cx } = useStyles();
return (
<div className="flex w-full items-center space-x-2.5">
<div className="flex items-center space-x-1">
<AppMaterialIcons className={styles.icon} icon="warning" />
<Text>Unsaved changes</Text>
</div>
<PopupSplitter />
<div className="flex items-center space-x-2">
<Button type="default" onClick={onReset}>
Reset
</Button>
<Button color="default" variant="solid" onClick={onSave}>
Save
</Button>
</div>
</div>
);
});
SaveResetFilePopup.displayName = 'SaveResetFilePopup';
SplitterContent.displayName = 'SplitterContent';
const useStyles = createStyles(({ css, token }) => ({
icon: css`
color: ${token.colorIcon};
`
}));

View File

@ -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<MetricViewProps> = React.memo(({ metricId }) => {
return <div>MetricViewFile</div>;
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 (
<div className="relative h-full overflow-hidden p-3">
<CodeCard code={file} language="yaml" fileName={file_name} onChange={setFile} />
<SaveResetFilePopup open={showPopup} onReset={onResetFile} onSave={onSaveFile} />
</div>
);
});
MetricViewFile.displayName = 'MetricViewFile';

View File

@ -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 ? <CardButtons fileName={fileName} code={code} /> : buttons;
return (
<div className={cx(styles.container, className)}>
<div
className={cx(
styles.containerHeader,
'flex items-center justify-between space-x-1 px-2.5'
)}>
<Text className="truncate">{fileName}</Text>
{ShownButtons}
</div>
<div className={cx(styles.containerBody, bodyClassName)}>
<AppCodeEditor
language={language}
value={code}
onChange={onChange}
readOnly={readOnly}
height="100%"
/>
</div>
</div>
);
};
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 (
<div className="flex items-center gap-1">
<Button
type="text"
icon={<AppMaterialIcons icon="content_copy" />}
onClick={handleCopyCode}
title="Copy code"
/>
<Button
type="text"
icon={<AppMaterialIcons icon="download" />}
onClick={handleDownload}
title="Download file"
/>
</div>
);
});
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};
`
}));

View File

@ -1 +1,2 @@
export * from './ItemContainer';
export * from './CodeCard';

View File

@ -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 (
<AnimatePresence mode="wait">
{show && (
<motion.div
className="absolute flex w-full justify-center"
initial={{ opacity: 0, y: -3 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -3 }}
transition={{ duration: 0.12 }}
style={{
bottom: 28
}}>
<div
style={{
backgroundColor: token.colorBgContainer,
borderRadius: token.borderRadius,
boxShadow: token.boxShadowSecondary,
padding: token.paddingXS
}}>
{show && (
<div className="flex w-full items-center space-x-2">
<SelectedButton selectedRowKeys={selectedRowKeys} onSelectChange={onSelectChange} />
<PopupContainer show={show}>
<div className="flex w-full items-center space-x-2">
<SelectedButton selectedRowKeys={selectedRowKeys} onSelectChange={onSelectChange} />
{buttons.length > 0 && (
<div
className="w-[0.5px]"
style={{ backgroundColor: token.colorSplit, height: token.controlHeight - 4 }}
/>
)}
{buttons.length > 0 && <PopupSplitter />}
{buttons.map((button, index) => (
<React.Fragment key={index}>{button}</React.Fragment>
))}
</div>
)}
</div>
</motion.div>
)}
</AnimatePresence>
{buttons.map((button, index) => (
<React.Fragment key={index}>{button}</React.Fragment>
))}
</div>
</PopupContainer>
);
};

View File

@ -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 (
<AnimatePresence mode="wait">
{show && (
<motion.div
className="absolute flex w-full justify-center"
initial={{ opacity: 0, y: -3 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -3 }}
transition={{ duration: 0.12 }}
style={{
bottom: 28
}}>
<div className={styles.container}>{show && <>{children}</>}</div>
</motion.div>
)}
</AnimatePresence>
);
};
export const PopupSplitter: React.FC<{}> = ({}) => {
const { styles, cx } = useStyles();
return <div className={styles.splitter} />;
};
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;
`
}));

View File

@ -0,0 +1 @@
export * from './PopupContainer';

View File

@ -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: '',

View File

@ -16,14 +16,16 @@ export const defaultIBusterMetric: Required<IBusterMetric> = {
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: '',

View File

@ -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<Record<keyof IBusterMetricChartConfig, (value: any) => any>> = {

View File

@ -100,7 +100,7 @@ export const useSQLProvider = () => {
return result;
} catch (error) {
console.log(error);
//
}
}
);