Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/enhance-data-source-select.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hyperdx/app": patch
---

feat: Enhance data source select with context-aware icons and inline actions
2 changes: 1 addition & 1 deletion packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"@mantine/notifications": "^7.17.8",
"@mantine/spotlight": "^7.17.8",
"@microsoft/fetch-event-source": "^2.0.1",
"@tabler/icons-react": "^3.5.0",
"@tabler/icons-react": "^3.39.0",
"@tanstack/react-query": "^5.56.2",
"@tanstack/react-query-devtools": "^5.56.2",
"@tanstack/react-table": "^8.7.9",
Expand Down
93 changes: 20 additions & 73 deletions packages/app/src/DBSearchPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
} from 'react';
import dynamic from 'next/dynamic';
import Head from 'next/head';
import Link from 'next/link';
import router from 'next/router';
import {
parseAsBoolean,
Expand Down Expand Up @@ -51,7 +50,6 @@ import {
Flex,
Grid,
Group,
Menu,
Modal,
Paper,
Select,
Expand All @@ -67,10 +65,8 @@ import {
import { notifications } from '@mantine/notifications';
import {
IconBolt,
IconCirclePlus,
IconPlayerPlay,
IconPlus,
IconSettings,
IconTags,
IconX,
} from '@tabler/icons-react';
Expand Down Expand Up @@ -191,59 +187,6 @@ export function getDefaultSourceId(
return sources[0].id;
}

function SourceEditMenu({
setModalOpen,
setModelFormExpanded,
}: {
setModalOpen: (val: SetStateAction<boolean>) => void;
setModelFormExpanded: (val: SetStateAction<boolean>) => void;
}) {
return (
<Menu withArrow position="bottom-start">
<Menu.Target>
<ActionIcon
data-testid="source-settings-menu"
variant="subtle"
size="sm"
title="Edit Source"
>
<Text size="xs">
<IconSettings size={14} />
</Text>
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<Menu.Label>Sources</Menu.Label>
<Menu.Item
data-testid="create-new-source-menu-item"
leftSection={<IconCirclePlus size={14} />}
onClick={() => setModalOpen(true)}
>
Create New Source
</Menu.Item>
{IS_LOCAL_MODE ? (
<Menu.Item
data-testid="edit-sources-menu-item"
leftSection={<IconSettings size={14} />}
onClick={() => setModelFormExpanded(v => !v)}
>
Edit Source
</Menu.Item>
) : (
<Menu.Item
data-testid="edit-sources-menu-item"
leftSection={<IconSettings size={14} />}
component={Link}
href="/team"
>
Edit Sources
</Menu.Item>
)}
</Menu.Dropdown>
</Menu>
);
}

function SourceEditModal({
opened,
onClose,
Expand Down Expand Up @@ -1568,6 +1511,14 @@ function DBSearchPage() {
setModelFormExpanded(false);
}, [setModelFormExpanded]);

const onEditSources = useCallback(() => {
if (IS_LOCAL_MODE) {
setModelFormExpanded(v => !v);
} else {
router.push('/team');
}
}, [setModelFormExpanded]);

const setNewSourceModalClosed = useCallback(
() => setNewSourceModalOpened(false),
[setNewSourceModalOpened],
Expand Down Expand Up @@ -1609,22 +1560,18 @@ function DBSearchPage() {
>
{/* <DevTool control={control} /> */}
<Flex gap="sm" px="sm" pt="sm" wrap="nowrap">
<Group gap="4px" wrap="nowrap" style={{ minWidth: 150 }}>
<SourceSelectControlled
key={`${savedSearchId}`}
size="xs"
control={control}
name="source"
onCreate={openNewSourceModal}
allowedSourceKinds={ALLOWED_SOURCE_KINDS}
data-testid="source-selector"
sourceSchemaPreview={sourceSchemaPreview}
/>
<SourceEditMenu
setModalOpen={setNewSourceModalOpened}
setModelFormExpanded={setModelFormExpanded}
/>
</Group>
<SourceSelectControlled
key={`${savedSearchId}`}
size="xs"
control={control}
name="source"
onCreate={openNewSourceModal}
onEdit={onEditSources}
allowedSourceKinds={ALLOWED_SOURCE_KINDS}
data-testid="source-selector"
sourceSchemaPreview={sourceSchemaPreview}
style={{ minWidth: 150 }}
/>
<Box style={{ flex: '1 1 0%', minWidth: 100 }}>
<SQLInlineEditorControlled
tableConnection={inputSourceTableConnection}
Expand Down
46 changes: 32 additions & 14 deletions packages/app/src/components/SelectControlled.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,15 @@ import { useCallback } from 'react';
import { useController, UseControllerProps } from 'react-hook-form';
import { Select, SelectProps } from '@mantine/core';

export enum SelectControlledSpecialValues {
CreateNewValue = '_create_new_value',
EditValue = '_edit_value',
}

export type SelectControlledProps = SelectProps &
UseControllerProps<any> & {
onCreate?: () => void;
onEdit?: () => void;
allowDeselect?: boolean;
};

Expand All @@ -19,35 +25,47 @@ export default function SelectControlled(props: SelectControlledProps) {
},
fieldState,
} = useController(props);
const { onCreate, allowDeselect = true, ...restProps } = props;
const { onCreate, onEdit, allowDeselect = true, ...restProps } = props;

// This is needed as mantine does not clear the select
// if the value is not in the data after
// if it was previously in the data (ex. data was deleted)
const selected = props.data?.find(d =>
typeof d === 'string'
? d === fieldValue
: 'value' in d
? d.value === fieldValue
: true,
);
// Mantine does not clear the select if the value is removed from data
// after it was previously present (ex. data was deleted)
const selected = props.data?.some(d => {
if (typeof d === 'string') return d === fieldValue;
if ('value' in d) return d.value === fieldValue;
if ('items' in d) {
return d.items.some(item =>
typeof item === 'string'
? item === fieldValue
: item.value === fieldValue,
);
}
return false;
});

const onChange = useCallback(
(value: string | null) => {
if (value === '_create_new_value' && onCreate != null) {
if (
value === SelectControlledSpecialValues.CreateNewValue &&
onCreate != null
) {
onCreate();
} else if (
value === SelectControlledSpecialValues.EditValue &&
onEdit != null
) {
onEdit();
} else if (value !== null || allowDeselect) {
fieldOnChange(value);
}
},
[fieldOnChange, onCreate, allowDeselect],
[fieldOnChange, onCreate, onEdit, allowDeselect],
);

return (
<Select
{...restProps}
error={fieldState.error?.message}
value={selected == null ? null : fieldValue}
value={selected ? fieldValue : null}
onChange={onChange}
onBlur={fieldOnBlur}
name={fieldName}
Expand Down
Loading
Loading