Merge pull request #34 from buster-so/nate/cleanup-permissions-pages

Cleanup permission pages 📃
This commit is contained in:
dal 2025-01-15 09:12:56 -08:00 committed by GitHub
commit 96dfae1b22
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 1066 additions and 195 deletions

900
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -66,6 +66,7 @@
"material-symbols": "^0.27.2",
"monaco-sql-languages": "^0.12.2",
"monaco-themes": "^0.4.4",
"monaco-yaml": "^5.2.3",
"next": "14.2.22",
"next-themes": "^0.4.4",
"next-transpile-modules": "^10.0.1",
@ -119,6 +120,7 @@
"eslint-config-next": "14.2.3",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"monaco-editor-webpack-plugin": "^7.1.0",
"postcss": "~8",
"sass": "^1.83.1",
"tailwindcss": "^3.4.17",

View File

@ -78,28 +78,14 @@ export const prefetchGetDatasetMetadata = async (
export const useCreateDataset = () => {
const queryClient = useQueryClient();
const mutationFn = useMemoizedFn(() => createDataset());
const onSuccess = useMemoizedFn((newDataset: unknown) => {
queryClient.setQueryData<BusterDatasetListItem[]>(['datasets', {}], (oldData) => {
// const newListItem: BusterDatasetListItem = {
// ...newDataset,
// name: newDataset.name,
// created_at: newDataset.created_at,
// updated_at: newDataset.updated_at,
// definition: newDataset.definition,
// owner: '',
// };
return oldData;
});
});
const onError = useMemoizedFn((error: any) => {
console.error('Failed to create dataset:', error);
const onSuccess = useMemoizedFn(() => {
queryClient.invalidateQueries({ queryKey: ['datasets', {}] });
});
return useCreateReactMutation({
mutationFn,
onSuccess,
onError
mutationFn: createDataset,
onSuccess
});
};

View File

@ -30,8 +30,11 @@ export const getDatasetDataSample = async (datasetId: string): Promise<BusterDat
.then((res) => res.data);
};
export const createDataset = async (): Promise<BusterDataset> => {
return await mainApi.post<BusterDataset>(`/datasets`).then((res) => res.data);
export const createDataset = async (params: {
name: string;
data_source_id: string;
}): Promise<BusterDataset> => {
return await mainApi.post<BusterDataset>(`/datasets`, params).then((res) => res.data);
};
export const deleteDataset = async (datasetId: string): Promise<void> => {

View File

@ -14,7 +14,7 @@ import { useDeployDataset } from '@/api/busterv2';
export const useDatasetPageContext = ({ datasetId }: { datasetId: string }) => {
const segments = useSelectedLayoutSegment() as DatasetApps;
const { mutate: onUpdateDataset } = useDeployDataset();
const { mutate: onUpdateDataset, isPending: isDeployingDataset } = useDeployDataset();
const { dataset, datasetData } = useIndividualDataset({ datasetId });
const originalDatasetSQL = dataset?.data?.sql;
const datasetYmlFile = dataset?.data?.yml_file;
@ -27,7 +27,7 @@ export const useDatasetPageContext = ({ datasetId }: { datasetId: string }) => {
const disablePublish = useMemo(() => {
const originalSQL = datasetSQL || '';
const originalYmlFile = datasetYmlFile || '';
return !datasetId || !sql || originalSQL === sql || !ymlFile || originalYmlFile === ymlFile;
return !datasetId || !sql || !ymlFile || (originalYmlFile === ymlFile && originalSQL === sql);
}, [datasetSQL, sql, datasetId, datasetYmlFile, ymlFile]);
const isChangedSQL = useMemo(() => {
@ -40,10 +40,11 @@ export const useDatasetPageContext = ({ datasetId }: { datasetId: string }) => {
});
const onPublishDataset = useMemoizedFn(async () => {
if (disablePublish || !sql || !ymlFile) return;
onUpdateDataset({
dataset_id: datasetId!,
sql: sql!,
yml: ymlFile!
sql: sql,
yml: ymlFile
});
});
@ -69,7 +70,8 @@ export const useDatasetPageContext = ({ datasetId }: { datasetId: string }) => {
dataset,
disablePublish,
isChangedSQL,
datasetId
datasetId,
isDeployingDataset
};
};

View File

@ -17,6 +17,7 @@ export const DatasetsIndividualHeader: React.FC<{}> = React.memo(({}) => {
const selectedApp = useDatasetPageContextSelector((state) => state.selectedApp);
const dataset = useDatasetPageContextSelector((state) => state.dataset);
const onPublishDataset = useDatasetPageContextSelector((state) => state.onPublishDataset);
const isDeployingDataset = useDatasetPageContextSelector((state) => state.isDeployingDataset);
const resetDataset = useDatasetPageContextSelector((state) => state.resetDataset);
const disablePublish = useDatasetPageContextSelector((state) => state.disablePublish);
const isChangedSQL = useDatasetPageContextSelector((state) => state.isChangedSQL);
@ -25,7 +26,6 @@ export const DatasetsIndividualHeader: React.FC<{}> = React.memo(({}) => {
const datasetName = dataset?.data?.name;
const isAdmin = useUserConfigContextSelector((state) => state.isAdmin);
const setOpenNewDatasetModal = useDatasetContextSelector((state) => state.setOpenNewDatasetModal);
const preventNavigation = !disablePublish;
@ -59,10 +59,14 @@ export const DatasetsIndividualHeader: React.FC<{}> = React.memo(({}) => {
<Divider type="vertical" className="!h-4" />
<div className="flex items-center space-x-2">
<Button type="text" onClick={onReset} disabled={!isChangedSQL}>
<Button type="text" onClick={onReset} disabled={!isChangedSQL || isDeployingDataset}>
Reset
</Button>
<Button type="primary" disabled={disablePublish} onClick={onPublishDataset}>
<Button
type="primary"
disabled={disablePublish}
onClick={onPublishDataset}
loading={isDeployingDataset}>
Publish
</Button>
</div>

View File

@ -1,4 +1,3 @@
import { BusterDataset } from '@/api/busterv2/datasets';
import { AppCodeEditor } from '@/components/inputs/AppCodeEditor';
import { createStyles } from 'antd-style';
import React from 'react';
@ -11,7 +10,12 @@ export const MetadataContainer: React.FC<{
return (
<div className={cx(styles.container, 'flex h-full w-full flex-col overflow-hidden')}>
<AppCodeEditor className="overflow-hidden" value={ymlFile} onChange={setYmlFile} />
<AppCodeEditor
language="yaml"
className="overflow-hidden"
value={ymlFile}
onChange={setYmlFile}
/>
</div>
);
});

View File

@ -1,22 +1,12 @@
import React, { useLayoutEffect, useMemo, useState } from 'react';
import { Button, Select, SelectProps } from 'antd';
import { Input, Select, SelectProps } from 'antd';
import { useMemoizedFn, useMount } from 'ahooks';
import { useDatasetContextSelector } from '@/context/Datasets';
import { useDataSourceContextSelector } from '@/context/DataSources';
import {
BusterDatasetListItem,
useCreateDataset,
useGetDatasets,
useUpdateDataset
} from '@/api/busterv2/datasets';
import { useCreateDataset } from '@/api/busterv2/datasets';
import { useAppLayoutContextSelector } from '@/context/BusterAppLayout';
import { BusterRoutes, createBusterRoute } from '@/routes';
import { useRouter } from 'next/navigation';
import { AppModal, Text } from '@/components';
import { useAntToken } from '@/styles/useAntToken';
import { BusterList, BusterListColumn, BusterListRow } from '@/components/list';
import { formatDate } from '@/utils/date';
import { timeout } from '@/utils';
const headerConfig = {
title: 'Create a dataset',
@ -35,21 +25,22 @@ export const NewDatasetModal: React.FC<{
const forceInitDataSourceList = useDataSourceContextSelector(
(state) => state.forceInitDataSourceList
);
const { mutateAsync: createDataset } = useCreateDataset();
const [creatingDataset, setCreatingDataset] = React.useState(false);
const { mutateAsync: createDataset, isPending: creatingDataset } = useCreateDataset();
const [selectedDatasource, setSelectedDatasource] = React.useState<string | null>(
datasourceId || null
);
const [datasetName, setDatasetName] = React.useState<string>('');
const disableSubmit = !selectedDatasource;
const disableSubmit = !selectedDatasource || !datasetName;
const createNewDatasetPreflight = useMemoizedFn(async () => {
if (creatingDataset || disableSubmit) return;
setCreatingDataset(true);
if (creatingDataset || disableSubmit || !selectedDatasource) return;
beforeCreate?.();
const res = await createDataset({
data_source_id: selectedDatasource!
data_source_id: selectedDatasource,
name: datasetName
});
if (res.id) {
onChangePage({
@ -62,16 +53,13 @@ export const NewDatasetModal: React.FC<{
afterCreate?.();
}, 150);
}
setTimeout(() => {
setCreatingDataset(false);
}, 500);
});
const onAddDataSourceClick = useMemoizedFn(() => {
onClose();
router.push(createBusterRoute({ route: BusterRoutes.SETTINGS_DATASOURCES_ADD }));
setTimeout(() => {
router.push(createBusterRoute({ route: BusterRoutes.SETTINGS_DATASOURCES_ADD }));
}, 350);
onClose();
}, 450);
});
useLayoutEffect(() => {
@ -98,14 +86,18 @@ export const NewDatasetModal: React.FC<{
return (
<AppModal open={open} onClose={onClose} header={headerConfig} footer={footerConfig}>
{open && (
<SelectDataSourceDropdown
setSelectedDatasource={setSelectedDatasource}
selectedDatasource={selectedDatasource}
/>
)}
<div className="mt-2 flex flex-col gap-3">
<FormWrapper title="Dataset name">
<DatasetNameInput setDatasetName={setDatasetName} datasetName={datasetName} />
</FormWrapper>
{open && selectedDatasource && (
<SelectFromExistingDataset selectedDatasource={selectedDatasource} />
<FormWrapper title="Datasource">
<SelectDataSourceDropdown
setSelectedDatasource={setSelectedDatasource}
selectedDatasource={selectedDatasource}
/>
</FormWrapper>
</div>
)}
</AppModal>
);
@ -132,6 +124,10 @@ const SelectDataSourceDropdown: React.FC<{
return selectOptions.find((option) => option.value === selectedDatasource);
}, [selectOptions, selectedDatasource]);
const onSelect = useMemoizedFn((value: unknown) => {
setSelectedDatasource(value as string);
});
useMount(() => {
initDataSourceList();
router.prefetch(
@ -149,115 +145,41 @@ const SelectDataSourceDropdown: React.FC<{
value={selectedOption}
placeholder="Select datasources that this term pertains to"
popupMatchSelectWidth={true}
autoFocus={true}
onChange={(value) => {
setSelectedDatasource(value as unknown as string);
}}
onChange={onSelect}
/>
);
});
SelectDataSourceDropdown.displayName = 'SelectDataSourceDropdown';
const SelectFromExistingDataset: React.FC<{
selectedDatasource: string;
}> = React.memo(({ selectedDatasource }) => {
const token = useAntToken();
const { data: importedDatasets, isFetched: isFetchedDatasets } = useGetDatasets({
imported: true
});
const { mutateAsync: onUpdateDataset } = useUpdateDataset();
const onChangePage = useAppLayoutContextSelector((s) => s.onChangePage);
const [submittingId, setSubmittingId] = useState<string | null>(null);
const columns: BusterListColumn[] = useMemo(() => {
return [
{
title: 'Name',
dataIndex: 'name'
},
{
title: 'Updated at',
dataIndex: 'updated_at',
render: (v) => formatDate({ date: v, format: 'lll' }),
width: 130
},
{
title: 'Actions',
dataIndex: 'actions',
width: 100,
render: (_, record: BusterDatasetListItem) => (
<div className="flex items-center justify-end">
<Button
loading={submittingId === record.id}
type="default"
onClick={() => onSelectDataset(record.id)}>
Use dataset
</Button>
</div>
)
}
];
}, [submittingId]);
const rows: BusterListRow[] = useMemo(() => {
return importedDatasets.map((dataset) => ({
id: dataset.id,
data: dataset
}));
}, [importedDatasets]);
const onSelectDataset = useMemoizedFn(async (datasetId: string) => {
setSubmittingId(datasetId);
// try {
// await onUpdateDataset({
// id: datasetId,
// name: 'test'
// });
// await timeout(500);
// onChangePage({
// route: BusterRoutes.APP_DATASETS_ID,
// datasetId
// });
// } catch (error) {
// setSubmittingId(null);
// }
});
const DatasetNameInput: React.FC<{
setDatasetName: (name: string) => void;
datasetName: string;
}> = React.memo(
({ setDatasetName, datasetName }) => {
return (
<Input
autoFocus
defaultValue={datasetName}
placeholder="Enter a name for your dataset"
onChange={(e) => setDatasetName(e.target.value)}
/>
);
},
() => true
);
DatasetNameInput.displayName = 'DatasetNameInput';
const FormWrapper: React.FC<{
title: string;
children: React.ReactNode;
}> = React.memo(({ title, children }) => {
return (
<div
className="mt-3 flex h-[250px] w-full flex-col"
style={{
border: `0.5px solid ${token.colorBorder}`,
borderRadius: token.borderRadius
}}>
<div
className="flex"
style={{
padding: 12,
background: token.controlItemBgActive,
borderBottom: `0.5px solid ${token.colorBorder}`
}}>
<Text size="sm">Use an existing table or view as a dataset</Text>
</div>
<div className="h-full w-full">
<BusterList
columns={columns}
rows={rows}
showHeader={false}
emptyState={
!isFetchedDatasets ? (
<div className="flex h-full w-full items-center justify-center">
<Text>No datasets found</Text>
</div>
) : (
<></>
)
}
/>
<div className="grid grid-cols-[minmax(150px,auto)_1fr] gap-4">
<div>
<Text>{title}</Text>
</div>
<div>{children}</div>
</div>
);
});
SelectFromExistingDataset.displayName = 'SelectFromExistingDataset';
FormWrapper.displayName = 'FormWrapper';

View File

@ -12,6 +12,9 @@ import { createStyles } from 'antd-style';
import { motion } from 'framer-motion';
import { useMemoizedFn } from 'ahooks';
import './MonacoWebWorker';
import { configureMonacoToUseYaml } from './yamlHelper';
//import GithubLightTheme from 'monaco-themes/themes/Github Light.json';
//import NightOwnTheme from 'monaco-themes/themes/Night Owl.json';
//https://github.com/brijeshb42/monaco-ace-tokenizer
@ -120,8 +123,13 @@ export const AppCodeEditor = forwardRef<AppCodeEditorHandle, AppCodeEditorProps>
(await import('./Github_light')).default,
(await import('./Tomorrow-Night')).default
]);
monaco.editor.defineTheme('github-light', GithubLightTheme as any);
monaco.editor.defineTheme('night-owl', NightOwlTheme as any);
if (language === 'yaml') {
await configureMonacoToUseYaml(monaco);
}
monaco.editor.defineTheme('github-light', GithubLightTheme);
monaco.editor.defineTheme('night-owl', NightOwlTheme);
editor.updateOptions({
theme: useDarkMode ? 'night-owl' : 'github-light'
});

View File

@ -1,9 +1,10 @@
import tailwind from '../../../../tailwind.config';
import type { editor } from 'monaco-editor';
const colors = tailwind.theme.extend.colors;
const editorBackground = '#ffffff';
const theme = {
const theme: editor.IStandaloneThemeData = {
base: 'vs',
inherit: true,
rules: [
@ -336,7 +337,14 @@ const theme = {
foreground: '032f62',
fontStyle: 'underline',
token: 'string.other.link'
}
},
{
foreground: '24292e',
token: 'string.yaml'
},
{ foreground: colors.buster.primary, token: 'type' },
{ foreground: '24292e', token: 'keyword.yaml' }, //booleans
{ foreground: 'b31d28', token: 'number.yaml' }
],
colors: {
'editor.foreground': '#24292e',

View File

@ -0,0 +1,37 @@
import { isServer } from '@tanstack/react-query';
if (!isServer && typeof window !== 'undefined') {
window.MonacoEnvironment = {
getWorker(moduleId, label) {
switch (label) {
case 'editorWorkerService':
return new Worker(new URL('monaco-editor/esm/vs/editor/editor.worker', import.meta.url));
case 'css':
case 'less':
case 'scss':
return new Worker(
new URL('monaco-editor/esm/vs/language/css/css.worker', import.meta.url)
);
case 'handlebars':
case 'html':
case 'razor':
return new Worker(
new URL('monaco-editor/esm/vs/language/html/html.worker', import.meta.url)
);
case 'json':
return new Worker(
new URL('monaco-editor/esm/vs/language/json/json.worker', import.meta.url)
);
case 'javascript':
case 'typescript':
return new Worker(
new URL('monaco-editor/esm/vs/language/typescript/ts.worker', import.meta.url)
);
case 'yaml':
return new Worker(new URL('monaco-yaml/yaml.worker', import.meta.url));
default:
throw new Error(`Unknown label ${label}`);
}
}
};
}

View File

@ -1,4 +1,6 @@
const theme = {
import type { editor } from 'monaco-editor';
const theme: editor.IStandaloneThemeData = {
inherit: true,
base: 'vs-dark',
rules: [
@ -339,6 +341,12 @@ const theme = {
token: 'storage'
}
],
colors: {
'editor.foreground': '#DCDCAA',
'editor.background': '#1E1E1E',
'editorCursor.foreground': '#DCDCAA',
'editorWhitespace.foreground': '#DCDCAA'
},
encodedTokensColors: []
};

View File

@ -0,0 +1,38 @@
export const configureMonacoToUseYaml = async (monaco: typeof import('monaco-editor')) => {
const { configureMonacoYaml } = await import('monaco-yaml');
configureMonacoYaml(monaco, {
enableSchemaRequest: true,
schemas: [
{
// If YAML file is opened matching this glob
fileMatch: ['**/.prettierrc.*'],
// Then this schema will be downloaded from the internet and used.
uri: 'https://json.schemastore.org/prettierrc.json'
},
{
// If YAML file is opened matching this glob
fileMatch: ['**/person.yaml'],
// The following schema will be applied
schema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'The persons display name'
},
age: {
type: 'integer',
description: 'How old is the person in years?'
},
occupation: {
enum: ['Delivery person', 'Software engineer', 'Astronaut']
}
}
},
// And the URI will be linked to as the source.
uri: 'https://github.com/remcohaszing/monaco-yaml#usage'
}
]
});
};

View File

@ -16,7 +16,6 @@ export const BusterList: React.FC<BusterListProps> = ({
onSelectChange,
emptyState,
showHeader = true,
columnHeaderVariant,
contextMenu,
showSelectAll = true
}) => {

View File

@ -15,7 +15,7 @@ export const BusterListRowComponentSelector = React.forwardRef<
selectedRowKeys?: string[];
rows: BusterListRow[];
style?: React.CSSProperties;
columnRowVariant: BusterListProps['columnRowVariant'];
columnRowVariant?: BusterListProps['columnRowVariant'];
}
>(
(