Shorpo 2024-02-17 14:25:11 -07:00 committed by GitHub
parent 95f5041c36
commit dba8a434be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 130 additions and 22 deletions

View file

@ -0,0 +1,5 @@
---
'@hyperdx/app': patch
---
Allow to drag and drop saved searches and dashhoards between groups

View file

@ -60,10 +60,10 @@ export async function updateDashboardAndAlerts(
query,
tags,
}: {
name: string;
charts: z.infer<typeof chartSchema>[];
query: string;
tags: z.infer<typeof tagsSchema>;
name?: string;
charts?: z.infer<typeof chartSchema>[];
query?: string;
tags?: z.infer<typeof tagsSchema>;
},
) {
const oldDashboard = await Dashboard.findOne({

View file

@ -95,9 +95,9 @@ router.put(
id: objectIdSchema,
}),
body: z.object({
name: z.string().max(1024),
charts: z.array(chartSchema),
query: z.string().max(2048),
name: z.string().max(1024).optional(),
charts: z.array(chartSchema).optional(),
query: z.string().max(2048).optional(),
tags: tagsSchema,
}),
}),

View file

@ -83,7 +83,7 @@ router.patch(
}),
body: z.object({
name: z.string().max(1024).min(1).optional(),
query: z.string().max(2048),
query: z.string().max(2048).optional(),
tags: tagsSchema,
}),
}),
@ -103,7 +103,7 @@ router.patch(
},
{
...(name && { name }),
query,
...(query && { query }),
tags: tags && uniq(tags),
},
{ new: true },

View file

@ -39,6 +39,9 @@ import { useLocalStorage, useWindowSize } from './utils';
import styles from '../styles/AppNav.module.scss';
const UNTAGGED_SEARCHES_GROUP_NAME = 'Saved Searches';
const UNTAGGED_DASHBOARDS_GROUP_NAME = 'Saved Dashboards';
const APP_PERFORMANCE_DASHBOARD_CONFIG = {
id: '',
name: 'App Performance',
@ -539,11 +542,13 @@ const AppNavLinkGroups = <T extends AppNavLinkItem>({
name,
groups,
renderLink,
onDragEnd,
forceExpandGroups = false,
}: {
name: string;
groups: AppNavLinkGroup<T>[];
renderLink: (item: T) => React.ReactNode;
onDragEnd?: (target: HTMLElement | null, newGroup: string | null) => void;
forceExpandGroups?: boolean;
}) => {
const [collapsedGroups, setCollapsedGroups] = useLocalStorage<
@ -560,10 +565,27 @@ const AppNavLinkGroups = <T extends AppNavLinkItem>({
[collapsedGroups, setCollapsedGroups],
);
const [draggingOver, setDraggingOver] = useState<string | null>(null);
return (
<>
{groups.map(group => (
<div key={group.name}>
<div
key={group.name}
className={cx(
draggingOver === group.name && styles.listGroupDragEnter,
)}
onDragOver={e => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
setDraggingOver(group.name);
}}
onDragEnd={e => {
e.preventDefault();
onDragEnd?.(e.target as HTMLElement, draggingOver);
setDraggingOver(null);
}}
>
<AppNavGroupLabel
onClick={() => handleToggleGroup(group.name)}
name={group.name}
@ -621,10 +643,20 @@ function useSearchableList<T extends AppNavLinkItem>({
if (untaggedItems.length) {
groupedItems[untaggedGroupName] = untaggedItems;
}
return Object.entries(groupedItems).map(([name, items]) => ({
name,
items,
}));
return Object.entries(groupedItems)
.map(([name, items]) => ({
name,
items,
}))
.sort((a, b) => {
if (a.name === untaggedGroupName) {
return 1;
}
if (b.name === untaggedGroupName) {
return -1;
}
return a.name.localeCompare(b.name);
});
}, [filteredList, untaggedGroupName]);
return {
@ -660,8 +692,14 @@ export default function AppNav({ fixed = false }: { fixed?: boolean }) {
} = api.useLogViews();
const logViews = logViewsData?.data ?? [];
const { data: dashboardsData, isLoading: isDashboardsLoading } =
api.useDashboards();
const updateDashboard = api.useUpdateDashboard();
const updateLogView = api.useUpdateLogView();
const {
data: dashboardsData,
isLoading: isDashboardsLoading,
refetch: refetchDashboards,
} = api.useDashboards();
const dashboards = dashboardsData?.data ?? [];
const { data: alertsData, isLoading: isAlertsLoading } = api.useAlerts();
@ -734,7 +772,7 @@ export default function AppNav({ fixed = false }: { fixed?: boolean }) {
groupedFilteredList: groupedFilteredSearchesList,
} = useSearchableList({
items: logViews,
untaggedGroupName: 'Saved Searches',
untaggedGroupName: UNTAGGED_SEARCHES_GROUP_NAME,
});
const [isSearchPresetsCollapsed, setSearchPresetsCollapsed] = useLocalStorage(
@ -749,7 +787,7 @@ export default function AppNav({ fixed = false }: { fixed?: boolean }) {
groupedFilteredList: groupedFilteredDashboardsList,
} = useSearchableList({
items: dashboards,
untaggedGroupName: 'Saved Dashboards',
untaggedGroupName: UNTAGGED_DASHBOARDS_GROUP_NAME,
});
const [isDashboardsPresetsCollapsed, setDashboardsPresetsCollapsed] =
@ -777,6 +815,8 @@ export default function AppNav({ fixed = false }: { fixed?: boolean }) {
lv._id === query.savedSearchId && styles.listLinkActive,
)}
title={lv.name}
draggable
data-savedsearchid={lv._id}
>
<div className="d-inline-block text-truncate">{lv.name}</div>
{Array.isArray(lv.alerts) && lv.alerts.length > 0 ? (
@ -802,6 +842,32 @@ export default function AppNav({ fixed = false }: { fixed?: boolean }) {
],
);
const handleLogViewDragEnd = useCallback(
(target: HTMLElement | null, name: string | null) => {
if (!target?.dataset.savedsearchid || name == null) {
return;
}
const logView = logViews.find(
lv => lv._id === target.dataset.savedsearchid,
);
if (logView?.tags?.includes(name)) {
return;
}
updateLogView.mutate(
{
id: target.dataset.savedsearchid,
tags: name === UNTAGGED_SEARCHES_GROUP_NAME ? [] : [name],
},
{
onSuccess: () => {
refetchLogViews();
},
},
);
},
[logViews, refetchLogViews, updateLogView],
);
const renderDashboardLink = useCallback(
(dashboard: Dashboard) => (
<Link
@ -811,6 +877,8 @@ export default function AppNav({ fixed = false }: { fixed?: boolean }) {
className={cx(styles.listLink, {
[styles.listLinkActive]: dashboard._id === query.dashboardId,
})}
draggable
data-dashboardid={dashboard._id}
>
{dashboard.name}
</Link>
@ -818,6 +886,32 @@ export default function AppNav({ fixed = false }: { fixed?: boolean }) {
[query.dashboardId],
);
const handleDashboardDragEnd = useCallback(
(target: HTMLElement | null, name: string | null) => {
if (!target?.dataset.dashboardid || name == null) {
return;
}
const dashboard = dashboards.find(
d => d._id === target.dataset.dashboardid,
);
if (dashboard?.tags?.includes(name)) {
return;
}
updateDashboard.mutate(
{
id: target.dataset.dashboardid,
tags: name === UNTAGGED_DASHBOARDS_GROUP_NAME ? [] : [name],
},
{
onSuccess: () => {
refetchDashboards();
},
},
);
},
[dashboards, refetchDashboards, updateDashboard],
);
return (
<>
<AuthLoadingBlocker />
@ -934,6 +1028,7 @@ export default function AppNav({ fixed = false }: { fixed?: boolean }) {
groups={groupedFilteredSearchesList}
renderLink={renderLogViewLink}
forceExpandGroups={!!searchesListQ}
onDragEnd={handleLogViewDragEnd}
/>
</div>
@ -1170,6 +1265,7 @@ export default function AppNav({ fixed = false }: { fixed?: boolean }) {
groups={groupedFilteredDashboardsList}
renderLink={renderDashboardLink}
forceExpandGroups={!!dashboardsListQ}
onDragEnd={handleDashboardDragEnd}
/>
{dashboards.length === 0 && (

View file

@ -536,7 +536,7 @@ const api = {
Error,
{
id: string;
query: string;
query?: string;
tags?: string[];
}
>(`log-views`, async ({ id, query, tags }) =>
@ -624,9 +624,9 @@ const api = {
HTTPError,
{
id: string;
name: string;
query: string;
charts: any[];
name?: string;
query?: string;
charts?: any[];
tags?: string[];
}
>(async ({ id, name, charts, query, tags }) =>

View file

@ -10,6 +10,12 @@
padding-bottom: 10px;
}
.listGroupDragEnter {
opacity: 0.7;
background: #ffffff1a;
border-radius: 10px;
}
.listGroupName {
color: $slate-400;
text-transform: uppercase;
@ -41,6 +47,7 @@
text-overflow: ellipsis;
white-space: nowrap;
padding-left: 16px;
user-select: none;
gap: 10px;
&:hover {
color: $slate-100;