diff --git a/.changeset/source-chip-kebab-menu.md b/.changeset/source-chip-kebab-menu.md new file mode 100644 index 0000000000..9ec99239ab --- /dev/null +++ b/.changeset/source-chip-kebab-menu.md @@ -0,0 +1,5 @@ +--- +'@hyperdx/app': patch +--- + +feat(source-picker): chip + kebab menu UX diff --git a/packages/app/src/DBSearchPage.tsx b/packages/app/src/DBSearchPage.tsx index 4e7a6a21de..2862d3b857 100644 --- a/packages/app/src/DBSearchPage.tsx +++ b/packages/app/src/DBSearchPage.tsx @@ -127,7 +127,9 @@ import DBSqlRowTableWithSideBar from './components/DBSqlRowTableWithSidebar'; import PatternTable from './components/PatternTable'; import { DBSearchHeatmapChart } from './components/Search/DBSearchHeatmapChart'; import DirectTraceSidePanel from './components/Search/DirectTraceSidePanel'; -import SourceSchemaPreview from './components/SourceSchemaPreview'; +import SourceSchemaPreview, { + isSourceSchemaPreviewEnabled, +} from './components/SourceSchemaPreview'; import { getRelativeTimeOptionLabel, LIVE_TAIL_DURATION_MS, @@ -1665,10 +1667,8 @@ export function DBSearchPage() { [inputSourceObj], ); - const sourceSchemaPreview = useMemo( - () => , - [inputSourceObj], - ); + const [isSourceSchemaPreviewOpen, setIsSourceSchemaPreviewOpen] = + useState(false); const onTimePickerSearch = useCallback( (range: string) => { @@ -1743,13 +1743,38 @@ export function DBSearchPage() { setModelFormExpanded(false); }, [setModelFormExpanded]); - const onEditSources = useCallback(() => { + // `Edit source` (singular): operate on the currently selected source. + // Local mode opens the inline edit modal seeded with `inputSource`; + // non-local uses a hard navigation so the page's `useQueryStates` + // (source/where/select/whereLanguage/filters/orderBy) can't merge + // stale /search state into the destination URL, and so + // `router.basePath` is correctly prepended for the /clickstack build. + const onEditCurrentSource = useCallback(() => { if (IS_LOCAL_MODE) { - setModelFormExpanded(v => !v); + setModelFormExpanded(true); + return; + } + if (inputSource) { + window.location.assign(`${router.basePath}/team?source=${inputSource}`); } else { - router.push('/team'); + window.location.assign(`${router.basePath}/team`); } - }, [setModelFormExpanded]); + }, [inputSource, setModelFormExpanded]); + + // `Manage sources`: open the all-sources list view. Only wired in + // non-local mode; local has no list-view surface so the menu item + // hides itself when this prop is undefined. We use `window.location` + // for a hard navigation instead of `router.push` so the page's + // `useQueryStates` (source/where/select/whereLanguage/filters/orderBy) + // can't restore its state into the new URL during the client-side + // transition, and so `router.basePath` is correctly prepended for + // the /clickstack build. + const onManageSources = useMemo(() => { + if (IS_LOCAL_MODE) return undefined; + return () => { + window.location.assign(`${router.basePath}/team`); + }; + }, []); const setNewSourceModalClosed = useCallback( () => setNewSourceModalOpened(false), @@ -1886,12 +1911,22 @@ export function DBSearchPage() { control={control} name="source" onCreate={openNewSourceModal} - onEdit={onEditSources} + onEdit={onEditCurrentSource} + onManageSources={onManageSources} + onSchemaPreview={() => setIsSourceSchemaPreviewOpen(true)} + isSchemaPreviewEnabled={isSourceSchemaPreviewEnabled( + inputSourceObj, + )} allowedSourceKinds={ALLOWED_SOURCE_KINDS} data-testid="source-selector" - sourceSchemaPreview={sourceSchemaPreview} style={{ minWidth: 150 }} /> + setIsSourceSchemaPreviewOpen(false)} + /> { if (watchedSource !== sourceId) { @@ -116,15 +120,22 @@ function DBServiceMapPage() { ); const sourceSelect = source ? ( - - } - /> + <> + setIsSourceSchemaPreviewOpen(true)} + isSchemaPreviewEnabled={isSourceSchemaPreviewEnabled(source)} + /> + setIsSourceSchemaPreviewOpen(false)} + /> + ) : null; const headerActions = ( diff --git a/packages/app/src/DashboardFiltersModal.tsx b/packages/app/src/DashboardFiltersModal.tsx index 94b5fcb77a..c773922d12 100644 --- a/packages/app/src/DashboardFiltersModal.tsx +++ b/packages/app/src/DashboardFiltersModal.tsx @@ -37,7 +37,9 @@ import SearchWhereInput, { import { SQLInlineEditorControlled } from '@/components/SQLEditor/SQLInlineEditor'; import { SourceMultiSelectControlled } from './components/SourceMultiSelect'; -import SourceSchemaPreview from './components/SourceSchemaPreview'; +import SourceSchemaPreview, { + isSourceSchemaPreviewEnabled, +} from './components/SourceSchemaPreview'; import { SourceSelectControlled } from './components/SourceSelect'; import { useSource, useSources } from './source'; import { getMetricTableName } from './utils'; @@ -139,6 +141,8 @@ const DashboardFilterEditForm = ({ const [modalContentRef, setModalContentRef] = useState( null, ); + const [isSourceSchemaPreviewOpen, setIsSourceSchemaPreviewOpen] = + useState(false); return ( - } + onSchemaPreview={() => setIsSourceSchemaPreviewOpen(true)} + isSchemaPreviewEnabled={isSourceSchemaPreviewEnabled(source)} disabled={!!presetSource} /> + setIsSourceSchemaPreviewOpen(false)} + /> {!presetSource && ( - } + onSchemaPreview={() => setIsLogSchemaPreviewOpen(true)} + isSchemaPreviewEnabled={isSourceSchemaPreviewEnabled(logSource)} + /> + setIsLogSchemaPreviewOpen(false)} /> - } + onSchemaPreview={() => setIsMetricSchemaPreviewOpen(true)} + isSchemaPreviewEnabled={isSourceSchemaPreviewEnabled(metricSource)} + /> + setIsMetricSchemaPreviewOpen(false)} /> ); diff --git a/packages/app/src/__tests__/DBSearchPage.directTrace.test.tsx b/packages/app/src/__tests__/DBSearchPage.directTrace.test.tsx index ac3b7e5c63..cb5327aa5c 100644 --- a/packages/app/src/__tests__/DBSearchPage.directTrace.test.tsx +++ b/packages/app/src/__tests__/DBSearchPage.directTrace.test.tsx @@ -208,7 +208,12 @@ jest.mock('../components/PatternTable', () => () =>
); jest.mock('../components/Search/DBSearchHeatmapChart', () => ({ DBSearchHeatmapChart: () =>
, })); -jest.mock('../components/SourceSchemaPreview', () => () =>
); +jest.mock('../components/SourceSchemaPreview', () => ({ + __esModule: true, + default: () =>
, + isSourceSchemaPreviewEnabled: () => false, + getSourceSchemaTables: () => [], +})); jest.mock('../components/Error/ErrorBoundary', () => ({ ErrorBoundary: ({ children }: { children: React.ReactNode }) => ( <>{children} diff --git a/packages/app/src/components/ChartEditor/RawSqlChartEditor.tsx b/packages/app/src/components/ChartEditor/RawSqlChartEditor.tsx index 04b9a2a746..fb3a441601 100644 --- a/packages/app/src/components/ChartEditor/RawSqlChartEditor.tsx +++ b/packages/app/src/components/ChartEditor/RawSqlChartEditor.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { Control, UseFormSetValue, useWatch } from 'react-hook-form'; import { TableConnection, @@ -31,7 +31,9 @@ import { DEFAULT_TILE_ALERT } from '@/utils/alerts'; import { ConnectionSelectControlled } from '../ConnectionSelect'; import { OnClickFormButton } from '../DBEditTimeChartForm/OnClickForm/OnClickFormButton'; -import SourceSchemaPreview from '../SourceSchemaPreview'; +import SourceSchemaPreview, { + isSourceSchemaPreviewEnabled, +} from '../SourceSchemaPreview'; import { SourceSelectControlled } from '../SourceSelect'; import { SQL_PLACEHOLDERS } from './constants'; @@ -137,9 +139,8 @@ export default function RawSqlChartEditor({ return [...paramCompletions, ...macroCompletions]; }, [displayType]); - const sourceSchemaPreview = useMemo(() => { - return ; - }, [sourceObject]); + const [isSourceSchemaPreviewOpen, setIsSourceSchemaPreviewOpen] = + useState(false); const tableConnections: TableConnection[] = useMemo(() => { if (!sources) return []; @@ -206,7 +207,14 @@ export default function RawSqlChartEditor({ size="xs" clearable placeholder="None" - sourceSchemaPreview={sourceSchemaPreview} + onSchemaPreview={() => setIsSourceSchemaPreviewOpen(true)} + isSchemaPreviewEnabled={isSourceSchemaPreviewEnabled(sourceObject)} + /> + setIsSourceSchemaPreviewOpen(false)} /> diff --git a/packages/app/src/components/DBEditTimeChartForm/ChartEditorControls.tsx b/packages/app/src/components/DBEditTimeChartForm/ChartEditorControls.tsx index fdaa884e8c..62948f7109 100644 --- a/packages/app/src/components/DBEditTimeChartForm/ChartEditorControls.tsx +++ b/packages/app/src/components/DBEditTimeChartForm/ChartEditorControls.tsx @@ -1,3 +1,4 @@ +import { useState } from 'react'; import { Control, FieldArrayWithId, @@ -25,7 +26,9 @@ import { } from '@/components/ChartEditor/types'; import MVOptimizationIndicator from '@/components/MaterializedViews/MVOptimizationIndicator'; import SearchWhereInput from '@/components/SearchInput/SearchWhereInput'; -import SourceSchemaPreview from '@/components/SourceSchemaPreview'; +import SourceSchemaPreview, { + isSourceSchemaPreviewEnabled, +} from '@/components/SourceSchemaPreview'; import { SourceSelectControlled } from '@/components/SourceSelect'; import { SQLInlineEditorControlled } from '@/components/SQLEditor/SQLInlineEditor'; import { IS_LOCAL_MODE } from '@/config'; @@ -91,6 +94,9 @@ export function ChartEditorControls({ openDisplaySettings, openHeatmapSettings, }: ChartEditorControlsProps) { + const [isSourceSchemaPreviewOpen, setIsSourceSchemaPreviewOpen] = + useState(false); + return ( <> @@ -108,9 +114,14 @@ export function ChartEditorControls({ ? [...HEATMAP_ALLOWED_SOURCE_KINDS] : undefined } - sourceSchemaPreview={ - - } + onSchemaPreview={() => setIsSourceSchemaPreviewOpen(true)} + isSchemaPreviewEnabled={isSourceSchemaPreviewEnabled(tableSource)} + /> + setIsSourceSchemaPreviewOpen(false)} /> diff --git a/packages/app/src/components/DBTableSelect.tsx b/packages/app/src/components/DBTableSelect.tsx index e76740536d..0228db08f7 100644 --- a/packages/app/src/components/DBTableSelect.tsx +++ b/packages/app/src/components/DBTableSelect.tsx @@ -1,11 +1,14 @@ +import { useState } from 'react'; import { useController, UseControllerProps } from 'react-hook-form'; -import { Flex, Select } from '@mantine/core'; +import { ComboboxChevron, Flex, Select } from '@mantine/core'; import { IconTable } from '@tabler/icons-react'; import { useTablesDirect } from '@/clickhouse'; -import SourceSchemaPreview from './SourceSchemaPreview'; -import { SourceSelectRightSection } from './SourceSelect'; +import SourceSchemaPreview, { + isSourceSchemaPreviewEnabled, +} from './SourceSchemaPreview'; +import { SourceManagementMenu } from './SourceSelect'; function DBTableSelect({ database, @@ -38,25 +41,22 @@ function DBTableSelect({ label: db.name, })); - const rightSectionProps = SourceSelectRightSection({ - sourceSchemaPreview: - connectionId && database && table ? ( - - ) : undefined, - }); + const [isSchemaPreviewOpen, setIsSchemaPreviewOpen] = useState(false); + const previewSource = + connectionId && database && table + ? { + connection: connectionId, + from: { databaseName: database, tableName: table }, + } + : undefined; return ( - +