mirror of https://github.com/buster-so/buster.git
querys for permissions
This commit is contained in:
parent
ddaf4e9347
commit
d96cb9ba68
|
@ -1,3 +1,4 @@
|
|||
export * from './responseInterfaces';
|
||||
export * from './requests';
|
||||
export * from './queryRequests';
|
||||
export * from './permissions';
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export const GET_PERMISSIONS_OVERVIEW = (datasetId: string) => `/datasets/${datasetId}/overview`;
|
|
@ -0,0 +1,3 @@
|
|||
export * from './requests';
|
||||
export * from './queryRequests';
|
||||
export * from './responseInterfaces';
|
|
@ -0,0 +1,103 @@
|
|||
import { useCreateReactQuery, useCreateReactMutation } from '@/api/createReactQuery';
|
||||
import {
|
||||
getDatasetPermissionsOverview,
|
||||
listDatasetGroups,
|
||||
listPermissionGroups,
|
||||
listPermissionUsers,
|
||||
updateDatasetGroups,
|
||||
updatePermissionGroups,
|
||||
updatePermissionUsers
|
||||
} from './requests';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { QueryClient } from '@tanstack/react-query';
|
||||
import { getDatasetPermissionsOverview_server } from './serverRequests';
|
||||
|
||||
export const useGetDatasetPermissionsOverview = (dataset_id: string) => {
|
||||
const queryFn = useMemoizedFn(() => getDatasetPermissionsOverview({ dataset_id }));
|
||||
|
||||
return useCreateReactQuery({
|
||||
queryKey: ['dataset_permissions_overview', dataset_id],
|
||||
queryFn
|
||||
});
|
||||
};
|
||||
|
||||
export const prefetchGetDatasetPermissionsOverview = async (
|
||||
datasetId: string,
|
||||
queryClientProp?: QueryClient
|
||||
) => {
|
||||
const queryClient = queryClientProp || new QueryClient();
|
||||
await queryClient.prefetchQuery({
|
||||
queryKey: ['dataset_permissions_overview', datasetId],
|
||||
queryFn: () => getDatasetPermissionsOverview_server(datasetId)
|
||||
});
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
export const useListPermissionGroups = (dataset_id: string) => {
|
||||
const queryFn = useMemoizedFn(() => listPermissionGroups({ dataset_id }));
|
||||
|
||||
return useCreateReactQuery({
|
||||
queryKey: ['list_permission_groups', dataset_id],
|
||||
queryFn
|
||||
});
|
||||
};
|
||||
|
||||
export const useListDatasetGroups = (dataset_id: string) => {
|
||||
const queryFn = useMemoizedFn(() => listDatasetGroups({ dataset_id }));
|
||||
|
||||
return useCreateReactQuery({
|
||||
queryKey: ['list_dataset_groups', dataset_id],
|
||||
queryFn
|
||||
});
|
||||
};
|
||||
|
||||
export const useListPermissionUsers = (dataset_id: string) => {
|
||||
const queryFn = useMemoizedFn(() => listPermissionUsers({ dataset_id }));
|
||||
|
||||
return useCreateReactQuery({
|
||||
queryKey: ['list_permission_users', dataset_id],
|
||||
queryFn
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdatePermissionGroups = (dataset_id: string) => {
|
||||
const mutationFn = useMemoizedFn((groups: { id: string; assigned: boolean }[]) =>
|
||||
updatePermissionGroups({ dataset_id, groups })
|
||||
);
|
||||
const onSuccess = useMemoizedFn(() => {
|
||||
// queryClient.invalidateQueries({ queryKey: ['dataset_permissions_overview', dataset_id] });
|
||||
});
|
||||
|
||||
return useCreateReactMutation({
|
||||
mutationFn,
|
||||
onSuccess
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateDatasetGroups = (dataset_id: string) => {
|
||||
const mutationFn = useMemoizedFn((groups: { id: string; assigned: boolean }[]) =>
|
||||
updateDatasetGroups({ dataset_id, groups })
|
||||
);
|
||||
const onSuccess = useMemoizedFn(() => {
|
||||
// queryClient.invalidateQueries({ queryKey: ['dataset_permissions_overview', dataset_id] });
|
||||
});
|
||||
|
||||
return useCreateReactMutation({
|
||||
mutationFn,
|
||||
onSuccess
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdatePermissionUsers = (dataset_id: string) => {
|
||||
const mutationFn = useMemoizedFn((users: { id: string; assigned: boolean }[]) =>
|
||||
updatePermissionUsers({ dataset_id, users })
|
||||
);
|
||||
const onSuccess = useMemoizedFn(() => {
|
||||
// queryClient.invalidateQueries({ queryKey: ['dataset_permissions_overview', dataset_id] });
|
||||
});
|
||||
|
||||
return useCreateReactMutation({
|
||||
mutationFn,
|
||||
onSuccess
|
||||
});
|
||||
};
|
|
@ -0,0 +1,73 @@
|
|||
import { mainApi } from '../../../buster';
|
||||
import {
|
||||
DatasetPermissionsOverviewResponse,
|
||||
ListDatasetGroupsResponse,
|
||||
ListPermissionGroupsResponse,
|
||||
ListPermissionUsersResponse
|
||||
} from './responseInterfaces';
|
||||
import * as config from './config';
|
||||
|
||||
export const listPermissionGroups = async ({
|
||||
dataset_id
|
||||
}: {
|
||||
dataset_id: string;
|
||||
}): Promise<ListPermissionGroupsResponse[]> => {
|
||||
return await mainApi.get(`/datasets/${dataset_id}/permission_groups`).then((res) => res.data);
|
||||
};
|
||||
|
||||
export const listDatasetGroups = async ({
|
||||
dataset_id
|
||||
}: {
|
||||
dataset_id: string;
|
||||
}): Promise<ListDatasetGroupsResponse[]> => {
|
||||
return await mainApi.get(`/datasets/${dataset_id}/dataset_groups`).then((res) => res.data);
|
||||
};
|
||||
|
||||
export const listPermissionUsers = async ({
|
||||
dataset_id
|
||||
}: {
|
||||
dataset_id: string;
|
||||
}): Promise<ListPermissionUsersResponse[]> => {
|
||||
return await mainApi.get(`/datasets/${dataset_id}/users`).then((res) => res.data);
|
||||
};
|
||||
|
||||
export const updatePermissionUsers = async ({
|
||||
dataset_id,
|
||||
users
|
||||
}: {
|
||||
dataset_id: string;
|
||||
users: {
|
||||
id: string;
|
||||
assigned: boolean;
|
||||
}[];
|
||||
}): Promise<void> => {
|
||||
return await mainApi.put(`/datasets/${dataset_id}/users`, users);
|
||||
};
|
||||
|
||||
export const updatePermissionGroups = async ({
|
||||
dataset_id,
|
||||
groups
|
||||
}: {
|
||||
dataset_id: string;
|
||||
groups: { id: string; assigned: boolean }[];
|
||||
}): Promise<void> => {
|
||||
return await mainApi.put(`/datasets/${dataset_id}/permission_groups`, groups);
|
||||
};
|
||||
|
||||
export const updateDatasetGroups = async ({
|
||||
dataset_id,
|
||||
groups
|
||||
}: {
|
||||
dataset_id: string;
|
||||
groups: { id: string; assigned: boolean }[];
|
||||
}): Promise<void> => {
|
||||
return await mainApi.put(`/datasets/${dataset_id}/dataset_groups`, groups);
|
||||
};
|
||||
|
||||
export const getDatasetPermissionsOverview = async ({
|
||||
dataset_id
|
||||
}: {
|
||||
dataset_id: string;
|
||||
}): Promise<DatasetPermissionsOverviewResponse> => {
|
||||
return await mainApi.get(config.GET_PERMISSIONS_OVERVIEW(dataset_id)).then((res) => res.data);
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
export interface ListPermissionGroupsResponse {
|
||||
id: string;
|
||||
name: string;
|
||||
assigned: boolean;
|
||||
}
|
||||
|
||||
export interface ListDatasetGroupsResponse {
|
||||
id: string;
|
||||
name: string;
|
||||
assigned: boolean;
|
||||
}
|
||||
|
||||
export interface ListPermissionUsersResponse {
|
||||
id: string;
|
||||
name: string;
|
||||
assigned: boolean;
|
||||
}
|
||||
|
||||
export interface DatasetPermissionOverviewUser {
|
||||
id: string;
|
||||
name: string;
|
||||
can_query: boolean;
|
||||
lineage: any[];
|
||||
}
|
||||
|
||||
export interface DatasetPermissionsOverviewResponse {
|
||||
dataset_id: string;
|
||||
users: DatasetPermissionOverviewUser[];
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
'use server';
|
||||
|
||||
import * as config from './config';
|
||||
import { serverFetch } from '../../../createServerInstance';
|
||||
import { DatasetPermissionsOverviewResponse } from './responseInterfaces';
|
||||
|
||||
export const getDatasetPermissionsOverview_server = async (datasetId: string) => {
|
||||
const response = await serverFetch<DatasetPermissionsOverviewResponse>(
|
||||
config.GET_PERMISSIONS_OVERVIEW(datasetId)
|
||||
);
|
||||
return response;
|
||||
};
|
|
@ -1,5 +1,11 @@
|
|||
import { useCreateReactMutation, useCreateReactQuery } from '@/api/createReactQuery';
|
||||
import { createDataset, getDatasetData, getDatasetMetadata, getDatasets } from './requests';
|
||||
import {
|
||||
createDataset,
|
||||
deployDataset,
|
||||
getDatasetDataSample,
|
||||
getDatasetMetadata,
|
||||
getDatasets
|
||||
} from './requests';
|
||||
import { BusterDataset, BusterDatasetData, BusterDatasetListItem } from './responseInterfaces';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { QueryClient, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
|
@ -36,24 +42,16 @@ export const prefetchGetDatasets = async (
|
|||
};
|
||||
|
||||
export const useGetDatasetData = (datasetId: string) => {
|
||||
const queryFn = useMemoizedFn(() => getDatasetData(datasetId));
|
||||
const queryFn = useMemoizedFn(() => getDatasetDataSample(datasetId));
|
||||
return useCreateReactQuery<BusterDatasetData>({
|
||||
queryKey: ['datasetData', datasetId],
|
||||
queryFn,
|
||||
enabled: !!datasetId,
|
||||
refetchOnMount: false
|
||||
refetchOnMount: false,
|
||||
staleTime: 1000 * 60 * 10 // 10 minutes
|
||||
});
|
||||
};
|
||||
|
||||
export const prefetchGetDatasetData = async (datasetId: string, queryClientProp?: QueryClient) => {
|
||||
const queryClient = queryClientProp || new QueryClient();
|
||||
await queryClient.prefetchQuery({
|
||||
queryKey: ['datasetData', datasetId],
|
||||
queryFn: () => getDatasetData(datasetId)
|
||||
});
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
export const useGetDatasetMetadata = (datasetId: string) => {
|
||||
const queryFn = useMemoizedFn(() => getDatasetMetadata(datasetId));
|
||||
return useCreateReactQuery<BusterDataset>({
|
||||
|
@ -100,3 +98,16 @@ export const useCreateDataset = () => {
|
|||
onError
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeployDataset = () => {
|
||||
const mutationFn = useMemoizedFn((params: { dataset_id: string; sql: string; yml: string }) =>
|
||||
deployDataset(params)
|
||||
);
|
||||
const onSuccess = useMemoizedFn((res: any) => {
|
||||
console.log(res);
|
||||
});
|
||||
return useCreateReactMutation({
|
||||
mutationFn,
|
||||
onSuccess
|
||||
});
|
||||
};
|
||||
|
|
|
@ -24,7 +24,7 @@ export const getDatasetMetadata = async (datasetId: string): Promise<BusterDatas
|
|||
.then((res) => res.data);
|
||||
};
|
||||
|
||||
export const getDatasetData = async (datasetId: string): Promise<BusterDatasetData> => {
|
||||
export const getDatasetDataSample = async (datasetId: string): Promise<BusterDatasetData> => {
|
||||
return await mainApi
|
||||
.get<BusterDatasetData>(`/datasets/${datasetId}/data/sample`)
|
||||
.then((res) => res.data);
|
||||
|
@ -37,3 +37,16 @@ export const createDataset = async (dataset: BusterDataset): Promise<BusterDatas
|
|||
export const deleteDataset = async (datasetId: string): Promise<void> => {
|
||||
return await mainApi.delete(`/datasets/${datasetId}`).then((res) => res.data);
|
||||
};
|
||||
|
||||
export const deployDataset = async ({
|
||||
dataset_id,
|
||||
...params
|
||||
}: {
|
||||
dataset_id: string;
|
||||
sql: string;
|
||||
yml: string;
|
||||
}): Promise<void> => {
|
||||
return await mainApi
|
||||
.post(`/datasets/deploy`, { id: dataset_id, ...params })
|
||||
.then((res) => res.data);
|
||||
};
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
'use server';
|
||||
|
||||
import { useSupabaseServerContext } from '@/context/Supabase/useSupabaseContext';
|
||||
import { BASE_URL } from './buster/instances';
|
||||
import type { RequestInit } from 'next/dist/server/web/spec-extension/request';
|
||||
import { createClient } from '../context/Supabase/server';
|
||||
|
||||
export interface FetchConfig extends RequestInit {
|
||||
baseURL?: string;
|
||||
|
@ -9,7 +10,9 @@ export interface FetchConfig extends RequestInit {
|
|||
}
|
||||
|
||||
export const serverFetch = async <T>(url: string, config: FetchConfig = {}): Promise<T> => {
|
||||
const { accessToken } = await useSupabaseServerContext();
|
||||
const supabase = await createClient();
|
||||
const sessionData = await supabase.auth.getSession();
|
||||
const accessToken = sessionData.data?.session?.access_token;
|
||||
|
||||
const { baseURL = BASE_URL, params, headers = {}, method = 'GET', ...restConfig } = config;
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { useGetDatasets } from '@/api/busterv2/datasets';
|
||||
import { useUserConfigContextSelector } from '@/context/Users';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { useIndividualDataset } from '@/context/Datasets';
|
||||
import { useSelectedLayoutSegment } from 'next/navigation';
|
||||
import React, { PropsWithChildren, useLayoutEffect, useState } from 'react';
|
||||
import React, { PropsWithChildren, useEffect, useLayoutEffect, useState } from 'react';
|
||||
import { DatasetApps } from './_config';
|
||||
import {
|
||||
createContext,
|
||||
|
@ -12,23 +12,24 @@ import {
|
|||
|
||||
export const useDatasetPageContext = ({ datasetId }: { datasetId: string }) => {
|
||||
const segments = useSelectedLayoutSegment() as DatasetApps;
|
||||
const datasetResult = useIndividualDataset({ datasetId });
|
||||
const datasetSQL = datasetResult.dataset.data?.sql;
|
||||
const datasetYmlFile = datasetResult.dataset.data?.yml_file;
|
||||
const [sql, setSQL] = useState<string>(datasetSQL || '');
|
||||
const { dataset, datasetData } = useIndividualDataset({ datasetId });
|
||||
const originalDatasetSQL = dataset.data?.sql;
|
||||
const datasetYmlFile = dataset.data?.yml_file;
|
||||
|
||||
const [sql, setSQL] = useState<string>(originalDatasetSQL || '');
|
||||
const [ymlFile, setYmlFile] = useState<string>(datasetYmlFile || '');
|
||||
|
||||
const selectedApp = segments;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setSQL(datasetSQL || '');
|
||||
}, [datasetSQL]);
|
||||
useEffect(() => {
|
||||
setSQL(originalDatasetSQL || '');
|
||||
}, [originalDatasetSQL]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
useEffect(() => {
|
||||
setYmlFile(datasetYmlFile || '');
|
||||
}, [datasetYmlFile]);
|
||||
|
||||
return { sql, ymlFile, selectedApp, setSQL, setYmlFile, ...datasetResult };
|
||||
return { sql, ymlFile, selectedApp, setSQL, setYmlFile, datasetData, dataset };
|
||||
};
|
||||
|
||||
const DatasetPageContext = createContext<ReturnType<typeof useDatasetPageContext>>(
|
||||
|
|
|
@ -16,9 +16,13 @@ export const DatasetsHeaderOptions: React.FC<{
|
|||
datasetId: string | undefined;
|
||||
}> = React.memo(({ datasetId, isAdmin, selectedApp }) => {
|
||||
const { push } = useRouter();
|
||||
const optionsItems = isAdmin
|
||||
? [DatasetApps.OVERVIEW, DatasetApps.PERMISSIONS, DatasetApps.EDITOR]
|
||||
: [DatasetApps.OVERVIEW, DatasetApps.PERMISSIONS];
|
||||
const optionsItems = useMemo(
|
||||
() =>
|
||||
isAdmin
|
||||
? [DatasetApps.OVERVIEW, DatasetApps.PERMISSIONS, DatasetApps.EDITOR]
|
||||
: [DatasetApps.OVERVIEW, DatasetApps.PERMISSIONS],
|
||||
[isAdmin]
|
||||
);
|
||||
|
||||
const options: SegmentedProps['options'] = useMemo(
|
||||
() =>
|
||||
|
|
|
@ -22,7 +22,7 @@ export const DatasetIndividualThreeDotMenu: React.FC<{
|
|||
}, [datasetId, onDeleteDataset]);
|
||||
|
||||
return (
|
||||
<Dropdown menu={menu}>
|
||||
<Dropdown menu={menu} trigger={['click']}>
|
||||
<Button type="text" icon={<AppMaterialIcons icon="more_horiz" />} />
|
||||
</Dropdown>
|
||||
);
|
||||
|
|
|
@ -3,7 +3,7 @@ import React from 'react';
|
|||
|
||||
export enum DatasetApps {
|
||||
OVERVIEW = 'overview',
|
||||
PERMISSIONS = 'PERMISSIONS',
|
||||
PERMISSIONS = 'permissions',
|
||||
EDITOR = 'editor'
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,6 @@ export const EditorContent: React.FC<{
|
|||
const ref = useRef<HTMLDivElement>(null);
|
||||
const splitterRef = useRef<AppSplitterRef>(null);
|
||||
const [selectedApp, setSelectedApp] = useState<EditorApps>(EditorApps.PREVIEW);
|
||||
const dataset = useDatasetPageContextSelector((state) => state.dataset);
|
||||
const datasetData = useDatasetPageContextSelector((state) => state.datasetData);
|
||||
const sql = useDatasetPageContextSelector((state) => state.sql);
|
||||
const setSQL = useDatasetPageContextSelector((state) => state.setSQL);
|
||||
|
@ -28,6 +27,7 @@ export const EditorContent: React.FC<{
|
|||
|
||||
const [tempData, setTempData] = useState<BusterDatasetData>(datasetData.data || []);
|
||||
const [fetchingTempData, setFetchingTempData] = useState(false);
|
||||
|
||||
const { runAsync: runQuery } = useRequest(
|
||||
async () => {
|
||||
await timeout(1000);
|
||||
|
|
|
@ -7,11 +7,8 @@ import { OverviewData } from './OverviewData';
|
|||
import { Divider } from 'antd';
|
||||
|
||||
export default function Page() {
|
||||
const selectedApp = useDatasetPageContextSelector((state) => state.selectedApp);
|
||||
const datasetRes = useDatasetPageContextSelector((state) => state.dataset);
|
||||
const datasetDataRes = useDatasetPageContextSelector((state) => state.datasetData);
|
||||
const sql = useDatasetPageContextSelector((state) => state.sql);
|
||||
const setSQL = useDatasetPageContextSelector((state) => state.setSQL);
|
||||
|
||||
const datasetData = datasetDataRes?.data;
|
||||
const dataset = datasetRes?.data;
|
||||
|
@ -37,7 +34,7 @@ export default function Page() {
|
|||
|
||||
<OverviewData
|
||||
datasetId={dataset.id}
|
||||
data={datasetData}
|
||||
data={datasetData || []}
|
||||
isFetchedDatasetData={isFetchedDatasetData}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
'use client';
|
||||
|
||||
import { useGetDatasetPermissionsOverview } from '@/api/busterv2/datasets';
|
||||
import React from 'react';
|
||||
|
||||
export const PermissionTitleCard: React.FC<{ datasetId: string }> = ({ datasetId }) => {
|
||||
const { data, isFetched } = useGetDatasetPermissionsOverview(datasetId);
|
||||
const users = data?.users;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isFetched ? (
|
||||
<div>
|
||||
<h1>Permission Title Card {users?.length}</h1>
|
||||
</div>
|
||||
) : (
|
||||
<div>Loading...</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -1,377 +0,0 @@
|
|||
import { useDatasetContextSelector } from '@/context/Datasets';
|
||||
import React, { useRef } from 'react';
|
||||
import { Input, InputRef } from 'antd';
|
||||
import { BusterDataset, BusterDatasetColumn } from '@/api/busterv2/datasets';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { useAntToken } from '@/styles/useAntToken';
|
||||
import { AppPopoverOption, AppPopoverOptions } from '@/components/tooltip/AppPopoverOptions';
|
||||
import { BsDatabaseDown, BsDatabaseSlash } from 'react-icons/bs';
|
||||
import { Text, Title } from '@/components';
|
||||
import { useUserConfigContextSelector } from '@/context/Users';
|
||||
import { useBusterNotifications } from '@/context/BusterNotifications';
|
||||
|
||||
export const DatasetDescriptions: React.FC<{
|
||||
selectedApp: string;
|
||||
selectedDataset: BusterDataset | undefined;
|
||||
sql: string | undefined;
|
||||
setSQL: (value: string) => void;
|
||||
isAdmin: boolean;
|
||||
}> = ({ selectedDataset }) => {
|
||||
return (
|
||||
<div className="flex flex-col space-y-5">
|
||||
{selectedDataset ? (
|
||||
<>
|
||||
<DatasetHeader
|
||||
title="Dataset descriptions"
|
||||
editableDescription={false}
|
||||
description={
|
||||
'Dataset descriptions are used to identify which dataset should be used to answer a user’s question. These descriptions should very clearly describe what insights the dataset can provide.'
|
||||
}
|
||||
/>
|
||||
<DatasetWhenToUseContainer dataset={selectedDataset} />
|
||||
<ColumnDescriptions dataset={selectedDataset} />
|
||||
</>
|
||||
) : (
|
||||
<div>{/* <Skeleton /> */}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const DatasetHeader: React.FC<{
|
||||
description: string;
|
||||
title: string;
|
||||
editableDescription?: boolean;
|
||||
onChange?: (value: string) => void;
|
||||
}> = ({ onChange, editableDescription = true, title, description }) => {
|
||||
const [isEditingTitle, onSetIsEditTitle] = React.useState(false);
|
||||
const [definition, onChangeDefinition] = React.useState(description);
|
||||
const token = useAntToken();
|
||||
|
||||
const handleClickAwayDescription = useMemoizedFn(() => {
|
||||
onSetIsEditTitle(false);
|
||||
const isChanged = definition !== description;
|
||||
if (isChanged) {
|
||||
onChange?.(definition);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex w-full justify-between space-x-2">
|
||||
<div className="flex w-full space-x-4">
|
||||
<div className="flex w-full flex-col space-y-2">
|
||||
<Title level={4}>{title}</Title>
|
||||
|
||||
{editableDescription ? (
|
||||
<Input.TextArea
|
||||
variant="borderless"
|
||||
className={'w-full !pl-0'}
|
||||
autoSize={{ maxRows: 15, minRows: 2 }}
|
||||
style={{
|
||||
color: isEditingTitle ? token.colorText : token.colorTextDescription
|
||||
}}
|
||||
defaultValue={definition}
|
||||
onChange={(e) => {
|
||||
onChangeDefinition(e.target.value);
|
||||
}}
|
||||
placeholder="Add dataset description..."
|
||||
onFocus={() => {
|
||||
onSetIsEditTitle(true);
|
||||
}}
|
||||
onBlur={() => {
|
||||
handleClickAwayDescription();
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Text type="secondary">{definition}</Text>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const DatasetWhenToUseContainer: React.FC<{ dataset: BusterDataset }> = ({ dataset }) => {
|
||||
const onUpdateDataset = useDatasetContextSelector((state) => state.onUpdateDataset);
|
||||
const isAdmin = useUserConfigContextSelector((state) => state.isAdmin);
|
||||
|
||||
const onUpdateWhenToUse = useMemoizedFn(
|
||||
(whenToUse: string, key: 'when_to_use' | 'when_not_to_use') => {
|
||||
const isChanged = whenToUse !== dataset[key];
|
||||
if (isChanged) {
|
||||
onUpdateDataset({
|
||||
id: dataset.id,
|
||||
[key]: whenToUse
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col space-y-4">
|
||||
<WhenToUseItem
|
||||
title="When to use this dataset..."
|
||||
placeholder='For example, "This dataset should be used for customer data"'
|
||||
whenToUse={dataset.when_to_use}
|
||||
isAdmin={isAdmin}
|
||||
onChange={(v) => {
|
||||
onUpdateWhenToUse(v, 'when_to_use');
|
||||
}}
|
||||
/>
|
||||
<WhenToUseItem
|
||||
title="When not to use this dataset..."
|
||||
placeholder='For example, "This dataset should not be used for customer data"'
|
||||
whenToUse={dataset.when_not_to_use}
|
||||
isAdmin={isAdmin}
|
||||
onChange={(v) => {
|
||||
onUpdateWhenToUse(v, 'when_not_to_use');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const WhenToUseItem: React.FC<{
|
||||
whenToUse: string;
|
||||
title: string;
|
||||
placeholder: string;
|
||||
onChange: (value: string) => void;
|
||||
isAdmin: boolean;
|
||||
}> = ({ title, isAdmin, whenToUse, placeholder, onChange }) => {
|
||||
const token = useAntToken();
|
||||
const inputRef = useRef<InputRef>(null);
|
||||
const [isEditingTitle, onSetIsEditTitle] = React.useState(false);
|
||||
|
||||
const handleClickAwayDescription = useMemoizedFn(() => {
|
||||
const value = inputRef.current?.input?.value;
|
||||
|
||||
if (value && value !== whenToUse) {
|
||||
onChange(value);
|
||||
}
|
||||
|
||||
onSetIsEditTitle(false);
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className="overflow-hidden"
|
||||
style={{
|
||||
borderRadius: `${token.borderRadius}px`,
|
||||
border: `0.5px solid ${token.colorBorder}`
|
||||
}}>
|
||||
<div
|
||||
className="px-4 py-2.5"
|
||||
style={{
|
||||
backgroundColor: token.controlItemBgActive,
|
||||
borderBottom: `0.5px solid ${token.colorSplit}`
|
||||
}}>
|
||||
<Text>{title}</Text>
|
||||
</div>
|
||||
<div className="px-4 py-5">
|
||||
<Input.TextArea
|
||||
ref={inputRef}
|
||||
variant="borderless"
|
||||
defaultValue={whenToUse}
|
||||
value={!isAdmin ? whenToUse : undefined}
|
||||
placeholder={placeholder}
|
||||
className={`!pl-0 ${!isAdmin ? '!cursor-text' : ''}`}
|
||||
disabled={!isAdmin}
|
||||
autoSize={{ maxRows: 8, minRows: 1 }}
|
||||
style={{
|
||||
color: isEditingTitle ? token.colorText : token.colorTextDescription
|
||||
}}
|
||||
onFocus={() => {
|
||||
if (isAdmin) onSetIsEditTitle(true);
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (isAdmin) handleClickAwayDescription();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ColumnDescriptions: React.FC<{ dataset: BusterDataset }> = ({ dataset }) => {
|
||||
const columns = dataset.columns;
|
||||
const onUpdateDatasetColumn = useDatasetContextSelector((state) => state.onUpdateDatasetColumn);
|
||||
const isAdmin = useUserConfigContextSelector((state) => state.isAdmin);
|
||||
const { openSuccessMessage } = useBusterNotifications();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col space-y-6"
|
||||
style={{
|
||||
marginTop: 64
|
||||
}}>
|
||||
<DatasetHeader
|
||||
editableDescription={false}
|
||||
title="Column descriptions"
|
||||
description={`We’ve generated descriptions for each of the columns in your dataset. You can edit these descriptions to explain business acronyms, when a column should be used, or how the data should be queried. You can also edit the column title to make them more descriptive & human-friendly.`}
|
||||
/>
|
||||
|
||||
<div>
|
||||
{columns.map((column) => (
|
||||
<ColumnDescription
|
||||
isAdmin={isAdmin}
|
||||
key={column.id}
|
||||
column={column}
|
||||
first={column.id === columns[0].id}
|
||||
last={column.id === columns[columns.length - 1].id}
|
||||
onEditDescription={(value) => {
|
||||
const isChanged = value !== column.name;
|
||||
if (isChanged) {
|
||||
onUpdateDatasetColumn({
|
||||
columnId: column.id,
|
||||
description: value
|
||||
});
|
||||
}
|
||||
}}
|
||||
onToggleStoredValues={async (value) => {
|
||||
const isChanged = value !== column.stored_values;
|
||||
if (isChanged) {
|
||||
await onUpdateDatasetColumn({
|
||||
columnId: column.id,
|
||||
stored_values: value
|
||||
});
|
||||
openSuccessMessage('Column updated');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ColumnDescription: React.FC<{
|
||||
first: boolean;
|
||||
last: boolean;
|
||||
column: BusterDataset['columns'][0];
|
||||
onEditDescription: (value: string) => void;
|
||||
onToggleStoredValues: (value: boolean) => void;
|
||||
isAdmin: boolean;
|
||||
}> = ({ column, isAdmin, first, onToggleStoredValues, last, onEditDescription }) => {
|
||||
const token = useAntToken();
|
||||
const [isEditing, onSetIsEditing] = React.useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex w-full justify-between space-x-3 px-4 py-3"
|
||||
style={{
|
||||
borderRadius:
|
||||
first && last
|
||||
? `${token.borderRadius}px`
|
||||
: first
|
||||
? `${token.borderRadius}px ${token.borderRadius}px 0 0`
|
||||
: last
|
||||
? `0 0 ${token.borderRadius}px ${token.borderRadius}px`
|
||||
: 0,
|
||||
border: `0.5px solid ${token.colorBorder}`,
|
||||
borderBottom: !last ? `0.5px solid transparent` : `0.5px solid ${token.colorBorder}`
|
||||
}}>
|
||||
<div className="flex w-full flex-col space-y-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Title level={4}>{column.name}</Title>
|
||||
|
||||
<div
|
||||
className="flex items-center"
|
||||
style={{
|
||||
height: 26,
|
||||
padding: '0px 6px ',
|
||||
color: token.colorTextDescription,
|
||||
borderRadius: `${token.borderRadius}px`,
|
||||
border: `0.5px solid ${token.colorSplit}`
|
||||
}}>
|
||||
{column.type}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Input.TextArea
|
||||
className={`w-full !pl-0 ${!isAdmin ? '!cursor-text' : ''}`}
|
||||
disabled={!isAdmin}
|
||||
defaultValue={column.description || ''}
|
||||
value={!isAdmin ? column.description || '' : undefined}
|
||||
style={{
|
||||
color: isEditing ? token.colorText : token.colorTextDescription
|
||||
}}
|
||||
variant="borderless"
|
||||
onFocus={() => {
|
||||
if (isAdmin) onSetIsEditing(true);
|
||||
}}
|
||||
autoSize={{ maxRows: 12, minRows: 1 }}
|
||||
placeholder={isAdmin ? 'Add column description...' : 'No description'}
|
||||
onBlur={(e) => {
|
||||
if (isAdmin) {
|
||||
const value = e.target.value;
|
||||
onSetIsEditing(false);
|
||||
onEditDescription(value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isAdmin && (
|
||||
<StoredValuesDropdown
|
||||
enabled={column.stored_values}
|
||||
onToggleStoredValues={onToggleStoredValues}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const StoredValuesDropdown: React.FC<{
|
||||
enabled: BusterDatasetColumn['stored_values'];
|
||||
onToggleStoredValues: (value: boolean) => void;
|
||||
}> = ({ enabled, onToggleStoredValues }) => {
|
||||
const positiveIcon = <BsDatabaseDown size={18} />;
|
||||
const negativeIcon = <BsDatabaseSlash size={18} />;
|
||||
|
||||
const storedValuesOptions: AppPopoverOption[] = [
|
||||
{
|
||||
key: 'not_selected',
|
||||
label: 'Don’t index this column’s data',
|
||||
description: 'This column will not be indexed and it’s data should never be stored.',
|
||||
icon: negativeIcon,
|
||||
onClick: () => {
|
||||
onToggleStoredValues(false);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'selected',
|
||||
label: 'Index this column’s data',
|
||||
description:
|
||||
'This column will be indexed. This column contains distinct string or enum values. This column doesn’t include any PII.',
|
||||
icon: positiveIcon,
|
||||
onClick: () => {
|
||||
onToggleStoredValues(true);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const selectedOption = enabled ? storedValuesOptions[1]! : storedValuesOptions[0]!;
|
||||
|
||||
return (
|
||||
<AppPopoverOptions
|
||||
options={storedValuesOptions}
|
||||
value={selectedOption}
|
||||
showCheckIcon={false}
|
||||
trigger={'click'}
|
||||
placement="bottomRight"
|
||||
footer={
|
||||
<div className="ml-10">
|
||||
Not sure?{' '}
|
||||
<a className="" target="_blank">
|
||||
Read the docs
|
||||
</a>
|
||||
</div>
|
||||
}>
|
||||
<div className={`relative h-fit cursor-pointer opacity-80 transition hover:opacity-100`}>
|
||||
{enabled ? positiveIcon : negativeIcon}
|
||||
</div>
|
||||
</AppPopoverOptions>
|
||||
);
|
||||
};
|
|
@ -1,26 +1,17 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { DatasetDescriptions } from './_DatasetDescriptions';
|
||||
import { useUserConfigContextSelector } from '@/context/Users';
|
||||
import { useDatasetPageContextSelector } from '../_DatasetPageContext';
|
||||
import { PermissionTitleCard } from './PermissionTitleCard';
|
||||
import { prefetchGetDatasetPermissionsOverview } from '@/api/busterv2/datasets/permissions/queryRequests';
|
||||
import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
|
||||
|
||||
export default function Page() {
|
||||
const sql = useDatasetPageContextSelector((state) => state.sql);
|
||||
const selectedApp = useDatasetPageContextSelector((state) => state.selectedApp);
|
||||
const dataset = useDatasetPageContextSelector((state) => state.dataset);
|
||||
const setSQL = useDatasetPageContextSelector((state) => state.setSQL);
|
||||
const isAdmin = useUserConfigContextSelector((state) => state.isAdmin);
|
||||
export default async function Page({ params }: { params: { datasetId: string } }) {
|
||||
const datasetId = params.datasetId;
|
||||
const queryClient = await prefetchGetDatasetPermissionsOverview(datasetId);
|
||||
|
||||
return (
|
||||
<div className="m-auto max-w-[1400px] overflow-y-auto px-14 pb-12 pt-12">
|
||||
<DatasetDescriptions
|
||||
setSQL={setSQL}
|
||||
sql={sql}
|
||||
isAdmin={isAdmin}
|
||||
selectedApp={selectedApp}
|
||||
selectedDataset={dataset}
|
||||
/>
|
||||
</div>
|
||||
<HydrationBoundary state={dehydrate(queryClient)}>
|
||||
<div className="m-auto max-w-[1400px] overflow-y-auto px-14 pb-12 pt-12">
|
||||
<PermissionTitleCard datasetId={datasetId} />
|
||||
</div>
|
||||
</HydrationBoundary>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -11,9 +11,9 @@ export const pathNameToRoute = (pathName: string, params: any): BusterRoutes =>
|
|||
[BusterRoutes.APP_DASHBOARD_ID]: BusterRoutes.APP_DASHBOARDS,
|
||||
[BusterRoutes.APP_COLLECTIONS_ID]: BusterRoutes.APP_COLLECTIONS,
|
||||
[BusterRoutes.APP_DATASETS_ID]: BusterRoutes.APP_DATASETS,
|
||||
[BusterRoutes.APP_DATASETS_ID_DESCRIPTIONS]: BusterRoutes.APP_DATASETS,
|
||||
[BusterRoutes.APP_DATASETS_ID_PERMISSIONS]: BusterRoutes.APP_DATASETS,
|
||||
[BusterRoutes.APP_DATASETS_ID_OVERVIEW]: BusterRoutes.APP_DATASETS,
|
||||
[BusterRoutes.APP_DATASETS_ID_SQL]: BusterRoutes.APP_DATASETS,
|
||||
[BusterRoutes.APP_DATASETS_ID_EDITOR]: BusterRoutes.APP_DATASETS,
|
||||
[BusterRoutes.APP_TERMS_ID]: BusterRoutes.APP_TERMS
|
||||
};
|
||||
if (route && paramRoutesToParent[route as string]) {
|
||||
|
|
Loading…
Reference in New Issue