mirror of https://github.com/buster-so/buster.git
Add to collection dropdown
This commit is contained in:
parent
4c474de977
commit
6be73bcf50
|
@ -32,13 +32,20 @@ export const useGetCollectionsList = (
|
|||
});
|
||||
};
|
||||
|
||||
export const useGetCollection = (collectionId: string | undefined) => {
|
||||
const useFetchCollection = () => {
|
||||
const getAssetPassword = useBusterAssetsContextSelector((state) => state.getAssetPassword);
|
||||
const { password } = getAssetPassword(collectionId!);
|
||||
|
||||
return useMemoizedFn(async (collectionId: string) => {
|
||||
const { password } = getAssetPassword(collectionId!);
|
||||
return collectionsGetCollection({ id: collectionId!, password });
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetCollection = (collectionId: string | undefined) => {
|
||||
const fetchCollection = useFetchCollection();
|
||||
return useQuery({
|
||||
...collectionQueryKeys.collectionsGetCollection(collectionId!),
|
||||
queryFn: () => collectionsGetCollection({ id: collectionId!, password }),
|
||||
queryFn: () => fetchCollection(collectionId!),
|
||||
enabled: !!collectionId
|
||||
});
|
||||
};
|
||||
|
@ -185,19 +192,21 @@ export const useUpdateCollectionShare = () => {
|
|||
});
|
||||
};
|
||||
|
||||
export const useAddAssetToCollection = () => {
|
||||
export const useAddAssetToCollection = (useInvalidate = true) => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: addAssetToCollection,
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: collectionQueryKeys.collectionsGetCollection(variables.id).queryKey
|
||||
});
|
||||
if (useInvalidate) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: collectionQueryKeys.collectionsGetCollection(variables.id).queryKey
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useRemoveAssetFromCollection = () => {
|
||||
export const useRemoveAssetFromCollection = (useInvalidate = true) => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: removeAssetFromCollection,
|
||||
|
@ -208,13 +217,79 @@ export const useRemoveAssetFromCollection = () => {
|
|||
queryClient.setQueryData(queryKey, (previousData) => {
|
||||
const ids = variables.assets.map((a) => a.id);
|
||||
return create(previousData!, (draft) => {
|
||||
draft.assets = draft.assets!.filter((a) => !ids.includes(a.id));
|
||||
draft.assets = draft.assets?.filter((a) => !ids.includes(a.id)) || [];
|
||||
});
|
||||
});
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
if (useInvalidate) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: collectionQueryKeys.collectionsGetCollection(variables.id).queryKey
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useAddAndRemoveAssetsFromCollection = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const fetchCollection = useFetchCollection();
|
||||
const { mutateAsync: addAssetToCollection } = useAddAssetToCollection(false);
|
||||
const { mutateAsync: removeAssetFromCollection } = useRemoveAssetFromCollection(false);
|
||||
|
||||
const addAndRemoveAssetsToCollection = useMemoizedFn(
|
||||
async (variables: {
|
||||
collectionId: string;
|
||||
assets: {
|
||||
type: 'metric' | 'dashboard';
|
||||
id: string;
|
||||
}[];
|
||||
}) => {
|
||||
let currentCollection = queryClient.getQueryData<BusterCollection>(
|
||||
collectionQueryKeys.collectionsGetCollection(variables.collectionId).queryKey
|
||||
);
|
||||
|
||||
if (!currentCollection) {
|
||||
currentCollection = await fetchCollection(variables.collectionId);
|
||||
queryClient.setQueryData(
|
||||
collectionQueryKeys.collectionsGetCollection(variables.collectionId).queryKey,
|
||||
currentCollection
|
||||
);
|
||||
}
|
||||
|
||||
if (!currentCollection) throw new Error('Collection not found');
|
||||
|
||||
const removedAssets =
|
||||
currentCollection.assets
|
||||
?.filter((a) => !variables.assets.some((b) => b.id === a.id))
|
||||
.map((a) => ({
|
||||
type: a.asset_type as 'metric' | 'dashboard',
|
||||
id: a.id
|
||||
})) || [];
|
||||
const addedAssets = variables.assets.filter(
|
||||
(a) => !currentCollection.assets?.some((b) => b.id === a.id)
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
addedAssets.length > 0 &&
|
||||
addAssetToCollection({
|
||||
id: variables.collectionId,
|
||||
assets: addedAssets
|
||||
}),
|
||||
removedAssets.length > 0 &&
|
||||
removeAssetFromCollection({
|
||||
id: variables.collectionId,
|
||||
assets: removedAssets
|
||||
})
|
||||
]);
|
||||
}
|
||||
);
|
||||
|
||||
return useMutation({
|
||||
mutationFn: addAndRemoveAssetsToCollection,
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: collectionQueryKeys.collectionsGetCollection(variables.id).queryKey
|
||||
queryKey: collectionQueryKeys.collectionsGetCollection(variables.collectionId).queryKey
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -0,0 +1,161 @@
|
|||
import { useGetMetricsList } from '@/api/buster_rest/metrics';
|
||||
import { useMemoizedFn } from '@/hooks';
|
||||
import React, { useLayoutEffect, useMemo, useState } from 'react';
|
||||
import { InputSelectModal, InputSelectModalProps } from '@/components/ui/modal/InputSelectModal';
|
||||
import { formatDate } from '@/lib';
|
||||
import { Button } from '@/components/ui/buttons';
|
||||
import { useGetDashboardsList } from '@/api/buster_rest/dashboards';
|
||||
import {
|
||||
useAddAndRemoveAssetsFromCollection,
|
||||
useGetCollection
|
||||
} from '@/api/buster_rest/collections';
|
||||
import { Text } from '@/components/ui/typography';
|
||||
import { ASSET_ICONS } from '../config/assetIcons';
|
||||
import pluralize from 'pluralize';
|
||||
|
||||
export const AddToCollectionModal: React.FC<{
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
collectionId: string;
|
||||
}> = React.memo(({ open, onClose, collectionId }) => {
|
||||
const { data: collection, isFetched: isFetchedCollection } = useGetCollection(collectionId);
|
||||
const { data: metrics, isFetched: isFetchedMetrics } = useGetMetricsList({});
|
||||
const { data: dashboards, isFetched: isFetchedDashboards } = useGetDashboardsList({});
|
||||
const { mutateAsync: addAndRemoveAssetsFromCollection } = useAddAndRemoveAssetsFromCollection();
|
||||
|
||||
const [selectedAssets, setSelectedAssets] = useState<string[]>([]);
|
||||
|
||||
const columns: InputSelectModalProps['columns'] = [
|
||||
{
|
||||
title: 'Name',
|
||||
dataIndex: 'name',
|
||||
render: (name, data: { type: 'metric' | 'dashboard' }) => {
|
||||
const Icon = data.type === 'metric' ? ASSET_ICONS.metrics : ASSET_ICONS.dashboards;
|
||||
return (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-icon-color">
|
||||
<Icon />
|
||||
</span>
|
||||
<Text>{name}</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Last edited',
|
||||
dataIndex: 'last_edited',
|
||||
width: 132,
|
||||
render: (value: string, x) => {
|
||||
return formatDate({
|
||||
date: value,
|
||||
format: 'lll'
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const rows = useMemo(() => {
|
||||
return [
|
||||
...(metrics?.map<{
|
||||
id: string;
|
||||
data: { name: string; type: 'metric'; last_edited: string };
|
||||
}>((metric) => ({
|
||||
id: metric.id,
|
||||
data: { name: metric.name, type: 'metric', last_edited: metric.last_edited }
|
||||
})) || []),
|
||||
...(dashboards?.map<{
|
||||
id: string;
|
||||
data: { name: string; type: 'dashboard'; last_edited: string };
|
||||
}>((dashboard) => ({
|
||||
id: dashboard.id,
|
||||
data: { name: dashboard.name, type: 'dashboard', last_edited: dashboard.last_edited }
|
||||
})) || [])
|
||||
];
|
||||
}, [metrics, dashboards]);
|
||||
|
||||
const handleAddAndRemoveMetrics = useMemoizedFn(async () => {
|
||||
const keyedAssets = rows.reduce<Record<string, { type: 'metric' | 'dashboard'; id: string }>>(
|
||||
(acc, asset) => {
|
||||
acc[asset.id] = { type: asset.data.type, id: asset.id };
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
const assets = selectedAssets.map<{ type: 'metric' | 'dashboard'; id: string }>((asset) => ({
|
||||
id: asset,
|
||||
type: keyedAssets[asset].type
|
||||
}));
|
||||
await addAndRemoveAssetsFromCollection({
|
||||
collectionId,
|
||||
assets
|
||||
});
|
||||
onClose();
|
||||
});
|
||||
|
||||
const originalIds = useMemo(() => {
|
||||
return collection?.assets?.map((asset) => asset.id) || [];
|
||||
}, [collection?.assets]);
|
||||
|
||||
const isSelectedChanged = useMemo(() => {
|
||||
const newIds = selectedAssets;
|
||||
return originalIds.length !== newIds.length || originalIds.some((id) => !newIds.includes(id));
|
||||
}, [originalIds, selectedAssets]);
|
||||
|
||||
const emptyState = useMemo(() => {
|
||||
if (!isFetchedMetrics || !isFetchedDashboards) {
|
||||
return 'Loading assets...';
|
||||
}
|
||||
if (rows.length === 0) {
|
||||
return 'No assets found';
|
||||
}
|
||||
return undefined;
|
||||
}, [isFetchedMetrics, isFetchedDashboards, rows]);
|
||||
|
||||
const footer: NonNullable<InputSelectModalProps['footer']> = useMemo(() => {
|
||||
return {
|
||||
left:
|
||||
selectedAssets.length > 0 ? (
|
||||
<Button variant="ghost" onClick={() => setSelectedAssets([])}>
|
||||
Clear selected
|
||||
</Button>
|
||||
) : undefined,
|
||||
secondaryButton: {
|
||||
text: 'Cancel',
|
||||
onClick: onClose
|
||||
},
|
||||
primaryButton: {
|
||||
text:
|
||||
selectedAssets.length === 0
|
||||
? 'Update collection'
|
||||
: `Add ${selectedAssets.length} ${pluralize('asset', selectedAssets.length)} to collection`,
|
||||
onClick: handleAddAndRemoveMetrics,
|
||||
disabled: !isSelectedChanged,
|
||||
tooltip: isSelectedChanged
|
||||
? `Adding ${selectedAssets.length} assets`
|
||||
: 'No changes to update'
|
||||
}
|
||||
};
|
||||
}, [selectedAssets.length, isSelectedChanged, handleAddAndRemoveMetrics]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (isFetchedCollection) {
|
||||
const assets = collection?.assets?.map((asset) => asset.id) || [];
|
||||
setSelectedAssets(assets);
|
||||
}
|
||||
}, [isFetchedCollection, collection?.assets]);
|
||||
|
||||
return (
|
||||
<InputSelectModal
|
||||
width={665}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
columns={columns}
|
||||
rows={rows}
|
||||
onSelectChange={setSelectedAssets}
|
||||
selectedRowKeys={selectedAssets}
|
||||
footer={footer}
|
||||
emptyState={emptyState}
|
||||
/>
|
||||
);
|
||||
});
|
|
@ -4,11 +4,7 @@ import React, { useLayoutEffect, useMemo, useState } from 'react';
|
|||
import { InputSelectModal, InputSelectModalProps } from '@/components/ui/modal/InputSelectModal';
|
||||
import { formatDate } from '@/lib';
|
||||
import { Button } from '@/components/ui/buttons';
|
||||
import {
|
||||
useAddAndRemoveMetricsFromDashboard,
|
||||
useAddMetricsToDashboard,
|
||||
useGetDashboard
|
||||
} from '@/api/buster_rest/dashboards';
|
||||
import { useAddAndRemoveMetricsFromDashboard, useGetDashboard } from '@/api/buster_rest/dashboards';
|
||||
|
||||
export const AddToDashboardModal: React.FC<{
|
||||
open: boolean;
|
||||
|
|
|
@ -1,452 +0,0 @@
|
|||
import React, { useLayoutEffect, useMemo, useRef } from 'react';
|
||||
import { Input } from '@/components/ui/inputs';
|
||||
import { AppSegmented } from '@/components/ui/segmented';
|
||||
import { Text } from '@/components/ui/typography';
|
||||
import { BusterList, BusterListColumn, BusterListRow } from '@/components/ui/list';
|
||||
import { useMemoizedFn, useThrottleFn } from '@/hooks';
|
||||
import { boldHighlights, formatDate } from '@/lib';
|
||||
import {
|
||||
type BusterDashboardResponse,
|
||||
type BusterSearchResult,
|
||||
ShareAssetType
|
||||
} from '@/api/asset_interfaces';
|
||||
import { CircleSpinnerLoaderContainer } from '@/components/ui/loaders';
|
||||
import { BusterCollection } from '@/api/asset_interfaces';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
|
||||
import { type SegmentedItem } from '@/components/ui/segmented';
|
||||
import { Speaker, Xmark } from '@/components/ui/icons';
|
||||
import { AppModal } from '@/components/ui/modal';
|
||||
import { Button } from '@/components/ui/buttons';
|
||||
import { Separator } from '@/components/ui/seperator';
|
||||
import { SearchParams } from '@/api/request_interfaces/search/interfaces';
|
||||
import { useUpdateCollection } from '@/api/buster_rest/collections';
|
||||
import { useUpdateDashboard } from '@/api/buster_rest/dashboards';
|
||||
|
||||
const filterOptions = [
|
||||
{ label: 'All', value: 'all' },
|
||||
{ label: 'Metrics', value: 'metrics' },
|
||||
{ label: 'Dashboards', value: 'dashboards' }
|
||||
];
|
||||
|
||||
export const AddTypeModal: React.FC<{
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
type?: 'collection' | 'dashboard';
|
||||
dashboardResponse?: BusterDashboardResponse;
|
||||
collection?: BusterCollection;
|
||||
}> = React.memo(({ type = 'collection', open, onClose, collection, dashboardResponse }) => {
|
||||
const { mutateAsync: updateCollection } = useUpdateCollection();
|
||||
const { mutateAsync: updateDashboard } = useUpdateDashboard();
|
||||
const [selectedFilter, setSelectedFilter] = React.useState<string>(filterOptions[0]!.value);
|
||||
const [inputValue, setInputValue] = React.useState<string>('');
|
||||
const [ongoingSearchItems, setOngoingSearchItems] = React.useState<BusterSearchResult[]>([]);
|
||||
const [initialSearchItems, setInitialSearchItems] = React.useState<BusterSearchResult[]>([]);
|
||||
const [loadedInitialSearchItems, setLoadedInitialSearchItems] = React.useState(false);
|
||||
const [selectedItemIds, setSelectedItemIds] = React.useState<Record<string, boolean>>({});
|
||||
const [submitting, setSubmitting] = React.useState(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const allSeenItems = useRef<Record<string, BusterSearchResult>>({});
|
||||
|
||||
const mountedInitial = useRef(false);
|
||||
|
||||
const dashboard = dashboardResponse?.dashboard;
|
||||
|
||||
const searchItems = inputValue ? ongoingSearchItems : ongoingSearchItems;
|
||||
|
||||
const columns: BusterListColumn[] = useMemo(() => {
|
||||
const fallbackName = (name: string, type: ShareAssetType) => {
|
||||
if (type === ShareAssetType.DASHBOARD && !name) {
|
||||
return 'New dashboard';
|
||||
}
|
||||
if (type === ShareAssetType.METRIC && !name) {
|
||||
return 'New metric';
|
||||
}
|
||||
|
||||
return name;
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
title: 'Name',
|
||||
dataIndex: 'name',
|
||||
render: (_, record: BusterSearchResult) => {
|
||||
const { name, type, highlights } = record;
|
||||
const icon = <Speaker />;
|
||||
const boldedText = boldHighlights(fallbackName(name, type), highlights);
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex items-center">{icon}</div>
|
||||
<Text>{boldedText}</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Updated at',
|
||||
dataIndex: 'updated_at',
|
||||
width: 125,
|
||||
render: (data) => formatDate({ date: data, format: 'lll' })
|
||||
}
|
||||
];
|
||||
}, []);
|
||||
|
||||
const rows: BusterListRow[] = useMemo(() => {
|
||||
return searchItems.map((item) => ({
|
||||
id: item.id,
|
||||
data: item,
|
||||
onClick: () => {
|
||||
setSelectedItemIds((prev) => ({ ...prev, [item.id]: !prev[item.id] }));
|
||||
}
|
||||
}));
|
||||
}, [searchItems]);
|
||||
|
||||
const selectedItemIdsKeys = useMemo(() => {
|
||||
return Object.entries(selectedItemIds)
|
||||
.filter(([, value]) => value)
|
||||
.map(([key]) => key);
|
||||
}, [selectedItemIds]);
|
||||
|
||||
const onSelectChange = useMemoizedFn((selectedRowKeys: string[]) => {
|
||||
const updatedSelectedItemIds = selectedRowKeys.reduce<Record<string, boolean>>((acc, curr) => {
|
||||
acc[curr] = true;
|
||||
return acc;
|
||||
}, {});
|
||||
setSelectedItemIds({ ...updatedSelectedItemIds });
|
||||
});
|
||||
|
||||
const addToAllSeenItems = useMemoizedFn((results: BusterSearchResult[]) => {
|
||||
results.forEach((item) => {
|
||||
allSeenItems.current[item.id] = item;
|
||||
});
|
||||
});
|
||||
|
||||
const onSetSelectedFilter = useMemoizedFn(async (value: string) => {
|
||||
setSelectedFilter(value);
|
||||
let results: BusterSearchResult[] = [];
|
||||
if (value === 'metrics') {
|
||||
results = await onSearchInput.run(inputValue, 'useMetrics');
|
||||
} else if (value === 'dashboards') {
|
||||
results = await onSearchInput.run(inputValue, 'useDashboards');
|
||||
} else {
|
||||
results = await onSearchInput.run(inputValue);
|
||||
}
|
||||
setInitialSearchItems(results);
|
||||
setLoadedInitialSearchItems(true);
|
||||
addToAllSeenItems(results);
|
||||
});
|
||||
|
||||
const onSearchInput = useThrottleFn(
|
||||
async (query: string, params?: 'useMetrics' | 'useDashboards') => {
|
||||
let results: BusterSearchResult[] = [];
|
||||
let include: (keyof SearchParams)[] = [];
|
||||
|
||||
if (type === 'collection') {
|
||||
include = ['exclude_dashboards', 'exclude_metrics'];
|
||||
if (params === 'useMetrics') {
|
||||
include = ['exclude_metrics'];
|
||||
} else if (params === 'useDashboards') {
|
||||
include = ['exclude_dashboards'];
|
||||
}
|
||||
|
||||
results = await onBusterSearch({
|
||||
query
|
||||
});
|
||||
} else if (type === 'dashboard') {
|
||||
include = ['exclude_metrics'];
|
||||
results = await onBusterSearch({
|
||||
query
|
||||
});
|
||||
}
|
||||
if (isEmpty(initialSearchItems) && !query) {
|
||||
setInitialSearchItems(results);
|
||||
addToAllSeenItems(results);
|
||||
}
|
||||
setLoadedInitialSearchItems(true);
|
||||
setOngoingSearchItems(results);
|
||||
addToAllSeenItems(results);
|
||||
return results;
|
||||
},
|
||||
{ wait: 400 }
|
||||
);
|
||||
|
||||
const initSelectedItems = useMemoizedFn(() => {
|
||||
if (dashboardResponse) {
|
||||
const objectMetrics = Object.values(dashboardResponse.metrics).reduce<
|
||||
Record<string, boolean>
|
||||
>((acc, metric) => {
|
||||
acc[metric.id] = true;
|
||||
return acc;
|
||||
}, {});
|
||||
setSelectedItemIds(objectMetrics);
|
||||
} else if (collection) {
|
||||
const objectMetrics = (collection.assets || []).reduce<Record<string, boolean>>(
|
||||
(acc, asset) => {
|
||||
acc[asset.id] = true;
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
setSelectedItemIds(objectMetrics);
|
||||
}
|
||||
});
|
||||
|
||||
const onSubmit = useMemoizedFn(async () => {
|
||||
setSubmitting(true);
|
||||
const selectedIds = Object.entries(selectedItemIds)
|
||||
.filter(([, value]) => value)
|
||||
.map(([key]) => key);
|
||||
|
||||
if (type === 'collection') {
|
||||
const assets = selectedIds.map((id) => {
|
||||
const type = allSeenItems.current[id]?.type;
|
||||
return {
|
||||
type,
|
||||
id
|
||||
};
|
||||
});
|
||||
await updateCollection({
|
||||
id: collection!.id,
|
||||
assets
|
||||
});
|
||||
} else if (type === 'dashboard') {
|
||||
console.log('TODO: add metrics to dashboard', dashboard!.id, selectedIds);
|
||||
await updateDashboard({
|
||||
id: dashboard!.id
|
||||
// metrics: selectedIds
|
||||
});
|
||||
}
|
||||
setSubmitting(false);
|
||||
return;
|
||||
});
|
||||
|
||||
const onModalOkay = useMemoizedFn(async () => {
|
||||
await onSubmit();
|
||||
onClose();
|
||||
});
|
||||
|
||||
const onBusterSearch = useMemoizedFn(async ({ query }: { query: string }) => {
|
||||
return [];
|
||||
});
|
||||
|
||||
const onChangeSearchInput = useMemoizedFn((value: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setInputValue(value.target.value);
|
||||
onSearchInput.run(value.target.value);
|
||||
});
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (open) {
|
||||
setInputValue('');
|
||||
initSelectedItems();
|
||||
setTimeout(() => {
|
||||
onSetSelectedFilter(filterOptions[0]!.value);
|
||||
}, 20);
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus();
|
||||
}, 150);
|
||||
}
|
||||
|
||||
if (!mountedInitial.current) {
|
||||
onSearchInput.run('');
|
||||
mountedInitial.current = true;
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<AppModal
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
width={800}
|
||||
header={{
|
||||
title: 'Add Metrics & Dashboards'
|
||||
}}
|
||||
footer={{
|
||||
primaryButton: {
|
||||
text: 'Apply',
|
||||
onClick: onModalOkay
|
||||
}
|
||||
}}>
|
||||
<ModalContent
|
||||
type={type}
|
||||
dashboardResponse={dashboardResponse}
|
||||
collection={collection}
|
||||
selectedFilter={selectedFilter}
|
||||
onSetSelectedFilter={onSetSelectedFilter}
|
||||
inputValue={inputValue}
|
||||
onChangeSearchInput={onChangeSearchInput}
|
||||
inputRef={inputRef}
|
||||
scrollContainerRef={scrollContainerRef}
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
loadedInitialSearchItems={loadedInitialSearchItems}
|
||||
selectedItemIdsKeys={selectedItemIdsKeys}
|
||||
onSelectChange={onSelectChange}
|
||||
onClose={onClose}
|
||||
/>
|
||||
|
||||
<ModalFooter
|
||||
type={type}
|
||||
onOk={onModalOkay}
|
||||
selectedItemIds={selectedItemIds}
|
||||
submitting={submitting}
|
||||
onCancel={onClose}
|
||||
dashboardResponse={dashboardResponse}
|
||||
collection={collection}
|
||||
/>
|
||||
</AppModal>
|
||||
);
|
||||
});
|
||||
|
||||
AddTypeModal.displayName = 'AddTypeModal';
|
||||
|
||||
const ModalContent: React.FC<{
|
||||
type: 'collection' | 'dashboard';
|
||||
dashboardResponse?: BusterDashboardResponse;
|
||||
collection?: BusterCollection;
|
||||
selectedFilter: string;
|
||||
onSetSelectedFilter: (value: string) => void;
|
||||
inputValue: string;
|
||||
onChangeSearchInput: (value: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
inputRef: React.RefObject<HTMLInputElement>;
|
||||
scrollContainerRef: React.RefObject<HTMLDivElement>;
|
||||
rows: BusterListRow[];
|
||||
columns: BusterListColumn[];
|
||||
loadedInitialSearchItems: boolean;
|
||||
selectedItemIdsKeys: string[];
|
||||
onSelectChange: (selectedRowKeys: string[]) => void;
|
||||
onClose: () => void;
|
||||
}> = React.memo(
|
||||
({
|
||||
type,
|
||||
selectedFilter,
|
||||
onSetSelectedFilter,
|
||||
inputValue,
|
||||
onChangeSearchInput,
|
||||
inputRef,
|
||||
scrollContainerRef,
|
||||
rows,
|
||||
columns,
|
||||
loadedInitialSearchItems,
|
||||
selectedItemIdsKeys,
|
||||
onSelectChange,
|
||||
onClose
|
||||
}) => {
|
||||
const onSetSelectedFiltersPreflight = useMemoizedFn((value: SegmentedItem) => {
|
||||
onSetSelectedFilter(value.value as string);
|
||||
});
|
||||
|
||||
const placeholder =
|
||||
type === 'collection'
|
||||
? 'Search for existing metrics and dashboards...'
|
||||
: 'Search for existing metrics...';
|
||||
|
||||
return (
|
||||
<>
|
||||
{type === 'collection' && (
|
||||
<div className="flex items-center justify-between px-4 py-2">
|
||||
<AppSegmented
|
||||
options={filterOptions}
|
||||
value={selectedFilter}
|
||||
onChange={onSetSelectedFiltersPreflight}
|
||||
/>
|
||||
|
||||
<Button variant="ghost" onClick={onClose} prefix={<Xmark />} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Separator className="my-0!" />
|
||||
|
||||
<div className="flex h-[48px] items-center space-x-2">
|
||||
<Input
|
||||
ref={inputRef}
|
||||
size="tall"
|
||||
className=""
|
||||
variant="ghost"
|
||||
placeholder={placeholder}
|
||||
value={inputValue}
|
||||
onChange={onChangeSearchInput}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{<Separator />}
|
||||
|
||||
<div
|
||||
className="max-h-[57vh] overflow-auto"
|
||||
ref={scrollContainerRef}
|
||||
style={{
|
||||
height: rows.length === 0 ? 150 : rows.length * 48 + 32
|
||||
}}>
|
||||
<BusterList
|
||||
selectedRowKeys={selectedItemIdsKeys}
|
||||
onSelectChange={onSelectChange}
|
||||
rows={rows}
|
||||
showSelectAll={false}
|
||||
columns={columns}
|
||||
emptyState={
|
||||
loadedInitialSearchItems ? (
|
||||
<div className="flex h-[200px] min-h-[200px] items-center justify-center">
|
||||
<Text variant="tertiary">No metrics or dashboards found</Text>
|
||||
</div>
|
||||
) : (
|
||||
<div className="min-h-[200px flex h-[200px] items-center justify-center">
|
||||
<CircleSpinnerLoaderContainer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ModalContent.displayName = 'ModalContent';
|
||||
const ModalFooter: React.FC<{
|
||||
onOk: () => void;
|
||||
onCancel: () => void;
|
||||
type: 'collection' | 'dashboard';
|
||||
submitting: boolean;
|
||||
selectedItemIds: Record<string, boolean>;
|
||||
dashboardResponse?: BusterDashboardResponse;
|
||||
collection?: BusterCollection;
|
||||
}> = React.memo(({ dashboardResponse, submitting, selectedItemIds, type, onOk, onCancel }) => {
|
||||
const copyText =
|
||||
type === 'collection'
|
||||
? `Select the metrics & dashboards that you would like to add to your collection.`
|
||||
: '';
|
||||
|
||||
const disabled = useMemo(() => {
|
||||
if (isEmpty(selectedItemIds)) return true;
|
||||
|
||||
if (dashboardResponse) {
|
||||
const metricIds = Object.values(dashboardResponse.metrics).map((metric) => metric.id);
|
||||
const allSelectedIds = Object.entries(selectedItemIds)
|
||||
.filter(([, value]) => value)
|
||||
.map(([key]) => key);
|
||||
if (metricIds.length !== allSelectedIds.length) return false;
|
||||
const allAreSelected = metricIds.every((id) => selectedItemIds[id]);
|
||||
return allAreSelected;
|
||||
}
|
||||
|
||||
return false;
|
||||
}, [selectedItemIds, dashboardResponse]);
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<Text variant="secondary">{copyText}</Text>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="ghost" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button disabled={disabled} loading={submitting} variant="primary" onClick={onOk}>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
ModalFooter.displayName = 'ModalFooter';
|
|
@ -1,6 +0,0 @@
|
|||
import React, { useRef } from 'react';
|
||||
|
||||
export const Test = () => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
return <div ref={ref}>Test</div>;
|
||||
};
|
|
@ -12,13 +12,13 @@ import {
|
|||
} from '@/api/asset_interfaces';
|
||||
import { Text } from '@/components/ui/typography';
|
||||
import { ListEmptyStateWithButton } from '@/components/ui/list';
|
||||
import { AddTypeModal } from '@/components/features/modal/AddTypeModal';
|
||||
import { ShareAssetType } from '@/api/asset_interfaces';
|
||||
import { useMemoizedFn } from '@/hooks';
|
||||
import { BusterList, BusterListColumn, BusterListRow } from '@/components/ui/list';
|
||||
import { CollectionIndividualSelectedPopup } from './CollectionsIndividualPopup';
|
||||
import { ASSET_ICONS } from '@/components/features/config/assetIcons';
|
||||
import { useUpdateCollection } from '@/api/buster_rest/collections';
|
||||
import { AddToCollectionModal } from '@/components/features/modal/AddToCollectionModal';
|
||||
|
||||
export const CollectionIndividualContent: React.FC<{
|
||||
collection: BusterCollection | undefined;
|
||||
|
@ -47,11 +47,10 @@ export const CollectionIndividualContent: React.FC<{
|
|||
loadedAsset={loadedAsset}
|
||||
/>
|
||||
|
||||
<AddTypeModal
|
||||
<AddToCollectionModal
|
||||
open={openAddTypeModal}
|
||||
onClose={onCloseModal}
|
||||
type="collection"
|
||||
collection={collection}
|
||||
collectionId={collection.id}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue