mirror of https://github.com/buster-so/buster.git
popup selet file container
This commit is contained in:
parent
2fc8c9a221
commit
1a373d1e04
|
@ -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;
|
||||
|
|
|
@ -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};
|
||||
`
|
||||
}));
|
|
@ -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';
|
||||
|
|
|
@ -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};
|
||||
`
|
||||
}));
|
|
@ -1 +1,2 @@
|
|||
export * from './ItemContainer';
|
||||
export * from './CodeCard';
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
`
|
||||
}));
|
|
@ -0,0 +1 @@
|
|||
export * from './PopupContainer';
|
|
@ -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: '',
|
||||
|
|
|
@ -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: '',
|
||||
|
|
|
@ -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>> = {
|
||||
|
|
|
@ -100,7 +100,7 @@ export const useSQLProvider = () => {
|
|||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
//
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue