mirror of https://github.com/buster-so/buster.git
update code card
This commit is contained in:
parent
f70338ba1f
commit
4db6adc757
|
@ -0,0 +1,98 @@
|
|||
import React, { forwardRef } from 'react';
|
||||
import { AppSplitter, type AppSplitterRef } from '@/components/ui/layouts/AppSplitter';
|
||||
import { SQLContainer } from './SQLContainer';
|
||||
import { DataContainer } from './DataContainer';
|
||||
import type { IDataResult } from '@/api/asset_interfaces';
|
||||
import { DiffSQLContainer } from './DiffSQLContainer';
|
||||
|
||||
const DEFAULT_LAYOUT = ['auto', '300px'];
|
||||
|
||||
export interface AppVerticalDiffCodeSplitterProps {
|
||||
originalValue: string;
|
||||
value: string;
|
||||
setValue: (value: string) => void;
|
||||
language: 'sql' | 'yaml';
|
||||
runSQLError: string | undefined;
|
||||
onRunQuery: () => Promise<void>;
|
||||
data: IDataResult;
|
||||
fetchingData: boolean;
|
||||
defaultLayout?: [string, string];
|
||||
autoSaveId: string;
|
||||
topHidden?: boolean;
|
||||
onSaveSQL?: () => Promise<void>;
|
||||
disabledSave?: boolean;
|
||||
gapAmount?: number;
|
||||
className?: string;
|
||||
fileName?: string;
|
||||
versionNumber?: number;
|
||||
}
|
||||
|
||||
export const AppVerticalDiffCodeSplitter = forwardRef<
|
||||
AppSplitterRef,
|
||||
AppVerticalDiffCodeSplitterProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
originalValue,
|
||||
value,
|
||||
setValue,
|
||||
language,
|
||||
runSQLError,
|
||||
onRunQuery,
|
||||
onSaveSQL,
|
||||
data,
|
||||
fetchingData,
|
||||
defaultLayout = DEFAULT_LAYOUT,
|
||||
autoSaveId,
|
||||
fileName,
|
||||
versionNumber,
|
||||
disabledSave = false,
|
||||
topHidden = false,
|
||||
gapAmount = 3,
|
||||
className
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
//tailwind might not like this, but yolo
|
||||
const sqlContainerClassName = !topHidden ? `mb-${gapAmount}` : '';
|
||||
const dataContainerClassName = !topHidden ? `mt-${gapAmount}` : '';
|
||||
|
||||
return (
|
||||
<AppSplitter
|
||||
ref={ref}
|
||||
leftChildren={
|
||||
<DiffSQLContainer
|
||||
className={sqlContainerClassName}
|
||||
originalValue={originalValue}
|
||||
value={value}
|
||||
setValue={setValue}
|
||||
language={language}
|
||||
error={runSQLError}
|
||||
onRunQuery={onRunQuery}
|
||||
onSaveSQL={onSaveSQL}
|
||||
disabledSave={disabledSave}
|
||||
fileName={fileName}
|
||||
versionNumber={versionNumber}
|
||||
/>
|
||||
}
|
||||
rightChildren={
|
||||
<DataContainer
|
||||
className={dataContainerClassName}
|
||||
data={data}
|
||||
fetchingData={fetchingData}
|
||||
/>
|
||||
}
|
||||
split="horizontal"
|
||||
defaultLayout={defaultLayout}
|
||||
autoSaveId={autoSaveId}
|
||||
preserveSide="left"
|
||||
rightPanelMinSize={'80px'}
|
||||
leftPanelMinSize={'120px'}
|
||||
leftHidden={topHidden}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
AppVerticalDiffCodeSplitter.displayName = 'AppVerticalDiffCodeSplitter';
|
|
@ -0,0 +1,114 @@
|
|||
'use client';
|
||||
|
||||
import { Command, ReturnKey } from '@/components/ui/icons';
|
||||
import { useBusterNotifications } from '@/context/BusterNotifications';
|
||||
import { useMemoizedFn } from '@/hooks';
|
||||
import { Button } from '@/components/ui/buttons/Button';
|
||||
import React, { useState } from 'react';
|
||||
import type { AppVerticalCodeSplitterProps } from './AppVerticalCodeSplitter';
|
||||
import { cn } from '@/lib/classMerge';
|
||||
import { ErrorClosableContainer } from '@/components/ui/error/ErrorClosableContainer';
|
||||
import { AppDiffCodeEditor } from '@/components/ui/inputs';
|
||||
import { Copy2 } from '@/components/ui/icons';
|
||||
import { Text } from '@/components/ui/typography';
|
||||
import { VersionPill } from '@/components/ui/tags/VersionPill';
|
||||
|
||||
export const DiffSQLContainer: React.FC<{
|
||||
className?: string;
|
||||
originalValue: string | undefined;
|
||||
value: string | undefined;
|
||||
setValue: (value: string) => void;
|
||||
onRunQuery: () => Promise<void>;
|
||||
onSaveSQL?: AppVerticalCodeSplitterProps['onSaveSQL'];
|
||||
disabledSave?: AppVerticalCodeSplitterProps['disabledSave'];
|
||||
error?: string | null;
|
||||
language: 'sql' | 'yaml';
|
||||
fileName?: string;
|
||||
versionNumber?: number;
|
||||
}> = React.memo(
|
||||
({
|
||||
language,
|
||||
disabledSave,
|
||||
className = '',
|
||||
originalValue,
|
||||
value,
|
||||
setValue,
|
||||
onRunQuery,
|
||||
onSaveSQL,
|
||||
error,
|
||||
fileName,
|
||||
versionNumber
|
||||
}) => {
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
const { openInfoMessage } = useBusterNotifications();
|
||||
|
||||
const onCopySQL = useMemoizedFn(() => {
|
||||
navigator.clipboard.writeText(value || '');
|
||||
openInfoMessage('Copied to clipboard');
|
||||
});
|
||||
|
||||
const onRunQueryPreflight = useMemoizedFn(async () => {
|
||||
setIsRunning(true);
|
||||
await onRunQuery();
|
||||
setIsRunning(false);
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-full w-full flex-col overflow-hidden',
|
||||
'bg-background rounded border',
|
||||
className
|
||||
)}>
|
||||
<div className="bg-item-select flex h-8 w-full items-center justify-between border-b px-2.5">
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
<Text>{fileName}</Text>
|
||||
{versionNumber && <VersionPill version_number={versionNumber} />}
|
||||
</div>
|
||||
<Button prefix={<Copy2 />} variant="ghost" onClick={onCopySQL} />
|
||||
</div>
|
||||
<AppDiffCodeEditor
|
||||
className="overflow-hidden"
|
||||
modified={value || ''}
|
||||
original={originalValue || ''}
|
||||
language={language}
|
||||
onChange={setValue}
|
||||
readOnly={true}
|
||||
/>
|
||||
<div className="relative hidden items-center justify-between border-t px-4 py-2.5">
|
||||
<Button onClick={onCopySQL}>Copy SQL</Button>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{onSaveSQL && (
|
||||
<Button
|
||||
disabled={disabledSave || !value || isRunning}
|
||||
variant="black"
|
||||
onClick={onSaveSQL}>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="default"
|
||||
loading={isRunning}
|
||||
disabled={!value}
|
||||
className="flex items-center space-x-0"
|
||||
onClick={onRunQueryPreflight}
|
||||
suffix={
|
||||
<div className="flex items-center gap-x-1 text-sm">
|
||||
<Command />
|
||||
<ReturnKey />
|
||||
</div>
|
||||
}>
|
||||
Run
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{error && <ErrorClosableContainer error={error} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
DiffSQLContainer.displayName = 'DiffSQLContainer';
|
|
@ -0,0 +1,60 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { DashboardFilterDiffModall } from './DashboardFilterDiffModall';
|
||||
import { fn } from '@storybook/test';
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
const meta = {
|
||||
title: 'Features/Modal/DashboardFilterDiffModall',
|
||||
component: DashboardFilterDiffModall,
|
||||
parameters: {
|
||||
layout: 'centered'
|
||||
},
|
||||
tags: ['autodocs']
|
||||
} satisfies Meta<typeof DashboardFilterDiffModall>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
// Helper to generate random SQL queries
|
||||
const generateSQLCode = () => {
|
||||
const tables = ['users', 'orders', 'products', 'customers', 'transactions'];
|
||||
const columns = ['id', 'name', 'email', 'created_at', 'status', 'price', 'quantity'];
|
||||
const conditions = [
|
||||
'active = true',
|
||||
"created_at > NOW() - INTERVAL '7 days'",
|
||||
"status = 'completed'",
|
||||
'price > 100'
|
||||
];
|
||||
|
||||
const randomTable = tables[Math.floor(Math.random() * tables.length)];
|
||||
const randomColumns = Array.from(
|
||||
{ length: Math.floor(Math.random() * 3) + 1 },
|
||||
() => columns[Math.floor(Math.random() * columns.length)]
|
||||
).join(', ');
|
||||
|
||||
const whereClause =
|
||||
Math.random() > 0.5 ? `WHERE ${conditions[Math.floor(Math.random() * conditions.length)]}` : '';
|
||||
|
||||
return `SELECT ${randomColumns} FROM ${randomTable} ${whereClause} LIMIT 100;`;
|
||||
};
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
open: true,
|
||||
onClose: fn(),
|
||||
metrics: Array.from({ length: 25 }, () => {
|
||||
// Generate two different SQL queries
|
||||
const originalSQL = generateSQLCode();
|
||||
const modifiedSQL = generateSQLCode();
|
||||
|
||||
return {
|
||||
id: faker.string.uuid(),
|
||||
name: faker.lorem.word(),
|
||||
description: faker.lorem.sentence(),
|
||||
code: modifiedSQL,
|
||||
original_code: originalSQL,
|
||||
version_number: faker.number.int({ min: 1, max: 100 })
|
||||
};
|
||||
})
|
||||
}
|
||||
};
|
|
@ -0,0 +1,166 @@
|
|||
import { Button } from '@/components/ui/buttons';
|
||||
import { Xmark } from '@/components/ui/icons';
|
||||
import { AppPageLayout } from '@/components/ui/layouts';
|
||||
import { Dialog, DialogContent } from '@/components/ui/modal/ModalBase';
|
||||
import { cn } from '@/lib/classMerge';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { AppVerticalDiffCodeSplitter } from '../layouts/AppVerticalCodeSplitter/AppVerticalDiffCodeSplitter';
|
||||
import { DialogTitle } from '@radix-ui/react-dialog';
|
||||
import { useMemoizedFn } from '@/hooks';
|
||||
import { Text, Title } from '@/components/ui/typography';
|
||||
|
||||
interface DashboardFilterDiffModallProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
metrics: {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
code: string;
|
||||
original_code: string;
|
||||
version_number: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
export const DashboardFilterDiffModall: React.FC<DashboardFilterDiffModallProps> = React.memo(
|
||||
({ open, onClose, metrics }) => {
|
||||
const [selectedMetricId, setSelectedMetricId] = useState<string | null>(null);
|
||||
|
||||
const selectedMetric = useMemo(() => {
|
||||
return metrics.find((metric) => metric.id === selectedMetricId);
|
||||
}, [metrics, selectedMetricId]);
|
||||
|
||||
const onSelectMetric = useMemoizedFn((metricId: string) => {
|
||||
setSelectedMetricId(metricId);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (metrics.length > 0) {
|
||||
setSelectedMetricId(metrics[0].id);
|
||||
}
|
||||
}, [metrics]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogTitle className="hidden">Dashboard Filter Diff Editor</DialogTitle>
|
||||
<DialogContent
|
||||
showClose={false}
|
||||
className="h-[80vh] max-h-[80vh] min-h-[75vh] w-full max-w-[1000px] min-w-[1000px] overflow-hidden">
|
||||
<div className="flex max-h-full w-full overflow-hidden">
|
||||
<Sidebar
|
||||
className="w-full max-w-[250px]"
|
||||
metrics={metrics}
|
||||
selectedMetricId={selectedMetricId}
|
||||
onSelectMetric={onSelectMetric}
|
||||
selectedMetric={selectedMetric}
|
||||
/>
|
||||
<Content className="flex-1" selectedMetric={selectedMetric} />
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
DashboardFilterDiffModall.displayName = 'DashboardFilterDiffModall';
|
||||
|
||||
const Sidebar: React.FC<{
|
||||
className?: string;
|
||||
metrics: DashboardFilterDiffModallProps['metrics'];
|
||||
selectedMetricId: string | null;
|
||||
selectedMetric: DashboardFilterDiffModallProps['metrics'][number] | null | undefined;
|
||||
onSelectMetric: (metricId: string) => void;
|
||||
}> = ({ selectedMetric, className, metrics, selectedMetricId, onSelectMetric }) => {
|
||||
const SidebarHeader = useMemo(() => {
|
||||
return (
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<Title as={'h5'}>{selectedMetric?.name}</Title>
|
||||
</div>
|
||||
);
|
||||
}, [selectedMetric?.name]);
|
||||
|
||||
return (
|
||||
<AppPageLayout
|
||||
headerBorderVariant="ghost"
|
||||
header={SidebarHeader}
|
||||
scrollable
|
||||
className={cn('border-r', className)}>
|
||||
<div className="mx-2 my-1.5 flex flex-col space-y-0.5">
|
||||
{metrics.map((metric) => (
|
||||
<SidebarItem
|
||||
key={metric.id}
|
||||
metric={metric}
|
||||
selectedMetricId={selectedMetricId}
|
||||
onSelectMetric={onSelectMetric}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</AppPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
const SidebarItem: React.FC<{
|
||||
className?: string;
|
||||
metric: DashboardFilterDiffModallProps['metrics'][number];
|
||||
selectedMetricId: string | null;
|
||||
onSelectMetric: (metricId: string) => void;
|
||||
}> = ({ className, metric, selectedMetricId, onSelectMetric }) => {
|
||||
return (
|
||||
<div
|
||||
onClick={() => onSelectMetric(metric.id)}
|
||||
className={cn(
|
||||
'hover:bg-item-hover flex h-11 cursor-pointer flex-col space-y-0.5 rounded px-2 py-1.5',
|
||||
selectedMetricId === metric.id && 'bg-item-select hover:bg-item-select',
|
||||
className
|
||||
)}>
|
||||
<Title as={'h5'}>{metric.name}</Title>
|
||||
<Text size={'sm'} truncate variant={'secondary'}>
|
||||
{metric.description}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Content: React.FC<{
|
||||
className?: string;
|
||||
selectedMetric: DashboardFilterDiffModallProps['metrics'][number] | null | undefined;
|
||||
}> = ({ className, selectedMetric }) => {
|
||||
return (
|
||||
<AppPageLayout
|
||||
headerClassName="px-3!"
|
||||
header={useMemo(
|
||||
() => (
|
||||
<div className="flex w-full items-center justify-end">
|
||||
<Button variant="ghost" prefix={<Xmark />} />
|
||||
</div>
|
||||
),
|
||||
[]
|
||||
)}
|
||||
className={cn('overflow-hidden', className)}>
|
||||
<div className="bg-item-hover h-full w-full p-5">
|
||||
{selectedMetric && (
|
||||
<AppVerticalDiffCodeSplitter
|
||||
originalValue={selectedMetric.original_code}
|
||||
value={selectedMetric.code}
|
||||
setValue={() => {}}
|
||||
language="sql"
|
||||
runSQLError={undefined}
|
||||
onRunQuery={() => Promise.resolve()}
|
||||
data={[]}
|
||||
defaultLayout={['300px', 'auto']}
|
||||
fetchingData={false}
|
||||
autoSaveId="dashboard-filter-diff-modal"
|
||||
fileName={selectedMetric.name}
|
||||
versionNumber={selectedMetric.version_number}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!selectedMetric && (
|
||||
<div className="bg-background flex h-full w-full items-center justify-center rounded border p-5">
|
||||
<Text variant={'secondary'}>Select a metric to view the diff</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AppPageLayout>
|
||||
);
|
||||
};
|
|
@ -14,7 +14,7 @@ export interface AppDiffCodeEditorProps {
|
|||
isDarkMode?: boolean;
|
||||
onMount?: (editor: editor.IStandaloneDiffEditor, monaco: typeof import('monaco-editor')) => void;
|
||||
original?: string;
|
||||
modified?: string;
|
||||
modified: string;
|
||||
onChange?: (value: string) => void;
|
||||
style?: React.CSSProperties;
|
||||
language?: string;
|
||||
|
@ -23,6 +23,7 @@ export interface AppDiffCodeEditorProps {
|
|||
monacoEditorOptions?: editor.IStandaloneDiffEditorConstructionOptions;
|
||||
variant?: 'bordered' | null;
|
||||
viewMode?: 'side-by-side' | 'inline';
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface AppDiffCodeEditorHandle {
|
||||
|
@ -34,7 +35,7 @@ export const AppDiffCodeEditor = forwardRef<AppDiffCodeEditorHandle, AppDiffCode
|
|||
{
|
||||
style,
|
||||
monacoEditorOptions,
|
||||
language = 'typescript',
|
||||
language = 'sql',
|
||||
className,
|
||||
readOnly,
|
||||
onChange,
|
||||
|
@ -45,7 +46,8 @@ export const AppDiffCodeEditor = forwardRef<AppDiffCodeEditorHandle, AppDiffCode
|
|||
modified = '',
|
||||
readOnlyMessage = 'Editing code is not allowed',
|
||||
variant,
|
||||
viewMode = 'side-by-side'
|
||||
viewMode = 'side-by-side',
|
||||
disabled = false
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
|
@ -57,7 +59,7 @@ export const AppDiffCodeEditor = forwardRef<AppDiffCodeEditorHandle, AppDiffCode
|
|||
return {
|
||||
originalEditable: false,
|
||||
automaticLayout: true,
|
||||
readOnly,
|
||||
readOnly: readOnly || disabled,
|
||||
renderSideBySide: viewMode === 'side-by-side',
|
||||
folding: false,
|
||||
lineDecorationsWidth: 15,
|
||||
|
@ -68,7 +70,7 @@ export const AppDiffCodeEditor = forwardRef<AppDiffCodeEditorHandle, AppDiffCode
|
|||
minimap: {
|
||||
enabled: false
|
||||
},
|
||||
renderSideBySideInlineBreakpoint: 600,
|
||||
renderSideBySideInlineBreakpoint: 400,
|
||||
compactMode: true,
|
||||
renderIndicators: false,
|
||||
onlyShowAccessibleDiffViewer: false,
|
||||
|
@ -81,7 +83,7 @@ export const AppDiffCodeEditor = forwardRef<AppDiffCodeEditorHandle, AppDiffCode
|
|||
},
|
||||
...monacoEditorOptions
|
||||
} satisfies editor.IStandaloneDiffEditorConstructionOptions;
|
||||
}, [readOnlyMessage, monacoEditorOptions, viewMode]);
|
||||
}, [readOnlyMessage, monacoEditorOptions, viewMode, readOnly, disabled]);
|
||||
|
||||
const onMountDiffEditor = useMemoizedFn(
|
||||
async (editor: editor.IStandaloneDiffEditor, monaco: typeof import('monaco-editor')) => {
|
||||
|
@ -106,7 +108,7 @@ export const AppDiffCodeEditor = forwardRef<AppDiffCodeEditorHandle, AppDiffCode
|
|||
|
||||
// Get the modified editor and add change listener
|
||||
const modifiedEditor = editor.getModifiedEditor();
|
||||
if (!readOnly) {
|
||||
if (!readOnly && !disabled) {
|
||||
modifiedEditor.onDidChangeModelContent(() => {
|
||||
onChange?.(modifiedEditor.getValue() || '');
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue