update code card

This commit is contained in:
Nate Kelley 2025-04-03 15:49:51 -06:00
parent f70338ba1f
commit 4db6adc757
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
5 changed files with 447 additions and 7 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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() || '');
});