From 4db6adc7575472350b86f15fabf6dccac40b79e7 Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Thu, 3 Apr 2025 15:49:51 -0600 Subject: [PATCH] update code card --- .../AppVerticalDiffCodeSplitter.tsx | 98 +++++++++++ .../DiffSQLContainer.tsx | 114 ++++++++++++ .../DashboardFilterDiffModall.stories.tsx | 60 +++++++ .../modal/DashboardFilterDiffModall.tsx | 166 ++++++++++++++++++ .../AppDiffCodeEditor/AppDiffCodeEditor.tsx | 16 +- 5 files changed, 447 insertions(+), 7 deletions(-) create mode 100644 web/src/components/features/layouts/AppVerticalCodeSplitter/AppVerticalDiffCodeSplitter.tsx create mode 100644 web/src/components/features/layouts/AppVerticalCodeSplitter/DiffSQLContainer.tsx create mode 100644 web/src/components/features/modal/DashboardFilterDiffModall.stories.tsx create mode 100644 web/src/components/features/modal/DashboardFilterDiffModall.tsx diff --git a/web/src/components/features/layouts/AppVerticalCodeSplitter/AppVerticalDiffCodeSplitter.tsx b/web/src/components/features/layouts/AppVerticalCodeSplitter/AppVerticalDiffCodeSplitter.tsx new file mode 100644 index 000000000..ef83ff2e9 --- /dev/null +++ b/web/src/components/features/layouts/AppVerticalCodeSplitter/AppVerticalDiffCodeSplitter.tsx @@ -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; + data: IDataResult; + fetchingData: boolean; + defaultLayout?: [string, string]; + autoSaveId: string; + topHidden?: boolean; + onSaveSQL?: () => Promise; + 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 ( + + } + rightChildren={ + + } + split="horizontal" + defaultLayout={defaultLayout} + autoSaveId={autoSaveId} + preserveSide="left" + rightPanelMinSize={'80px'} + leftPanelMinSize={'120px'} + leftHidden={topHidden} + className={className} + /> + ); + } +); + +AppVerticalDiffCodeSplitter.displayName = 'AppVerticalDiffCodeSplitter'; diff --git a/web/src/components/features/layouts/AppVerticalCodeSplitter/DiffSQLContainer.tsx b/web/src/components/features/layouts/AppVerticalCodeSplitter/DiffSQLContainer.tsx new file mode 100644 index 000000000..36c68dfc8 --- /dev/null +++ b/web/src/components/features/layouts/AppVerticalCodeSplitter/DiffSQLContainer.tsx @@ -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; + 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 ( +
+
+
+ {fileName} + {versionNumber && } +
+
+ +
+ + +
+ {onSaveSQL && ( + + )} + +
+ }> + Run + +
+ + {error && } +
+ + ); + } +); + +DiffSQLContainer.displayName = 'DiffSQLContainer'; diff --git a/web/src/components/features/modal/DashboardFilterDiffModall.stories.tsx b/web/src/components/features/modal/DashboardFilterDiffModall.stories.tsx new file mode 100644 index 000000000..7df395adc --- /dev/null +++ b/web/src/components/features/modal/DashboardFilterDiffModall.stories.tsx @@ -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; + +export default meta; +type Story = StoryObj; + +// 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 }) + }; + }) + } +}; diff --git a/web/src/components/features/modal/DashboardFilterDiffModall.tsx b/web/src/components/features/modal/DashboardFilterDiffModall.tsx new file mode 100644 index 000000000..9cf200cc3 --- /dev/null +++ b/web/src/components/features/modal/DashboardFilterDiffModall.tsx @@ -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 = React.memo( + ({ open, onClose, metrics }) => { + const [selectedMetricId, setSelectedMetricId] = useState(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 ( + + Dashboard Filter Diff Editor + +
+ + +
+
+
+ ); + } +); + +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 ( +
+ {selectedMetric?.name} +
+ ); + }, [selectedMetric?.name]); + + return ( + +
+ {metrics.map((metric) => ( + + ))} +
+
+ ); +}; + +const SidebarItem: React.FC<{ + className?: string; + metric: DashboardFilterDiffModallProps['metrics'][number]; + selectedMetricId: string | null; + onSelectMetric: (metricId: string) => void; +}> = ({ className, metric, selectedMetricId, onSelectMetric }) => { + return ( +
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 + )}> + {metric.name} + + {metric.description} + +
+ ); +}; + +const Content: React.FC<{ + className?: string; + selectedMetric: DashboardFilterDiffModallProps['metrics'][number] | null | undefined; +}> = ({ className, selectedMetric }) => { + return ( + ( +
+
+ ), + [] + )} + className={cn('overflow-hidden', className)}> +
+ {selectedMetric && ( + {}} + 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 && ( +
+ Select a metric to view the diff +
+ )} +
+
+ ); +}; diff --git a/web/src/components/ui/inputs/AppDiffCodeEditor/AppDiffCodeEditor.tsx b/web/src/components/ui/inputs/AppDiffCodeEditor/AppDiffCodeEditor.tsx index 23f2064db..025a3ada7 100644 --- a/web/src/components/ui/inputs/AppDiffCodeEditor/AppDiffCodeEditor.tsx +++ b/web/src/components/ui/inputs/AppDiffCodeEditor/AppDiffCodeEditor.tsx @@ -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 { @@ -57,7 +59,7 @@ export const AppDiffCodeEditor = forwardRef { @@ -106,7 +108,7 @@ export const AppDiffCodeEditor = forwardRef { onChange?.(modifiedEditor.getValue() || ''); });