mirror of https://github.com/buster-so/buster.git
212 lines
6.4 KiB
TypeScript
212 lines
6.4 KiB
TypeScript
'use client';
|
|
|
|
import React, { useEffect, useMemo, useState } from 'react';
|
|
import { AppContent } from '@/components/ui/layout/AppContent';
|
|
import { useBusterTermsIndividualContextSelector, useBusterTermsIndividual } from '@/context/Terms';
|
|
import { Dropdown, Input } from 'antd';
|
|
import { useDebounceFn } from 'ahooks';
|
|
import { formatDate } from '@/lib';
|
|
import { AppMaterialIcons, EditableTitle } from '@/components/ui';
|
|
import { useAntToken } from '@/styles/useAntToken';
|
|
import { AppCodeEditor } from '@/components/ui/inputs/AppCodeEditor';
|
|
import clamp from 'lodash/clamp';
|
|
import { MenuProps } from 'antd/lib';
|
|
import { ItemContainer } from '@/components/ui/card/ItemContainer';
|
|
import { Text } from '@/components/ui';
|
|
import { BusterRoutes } from '@/routes';
|
|
import { useAppLayoutContextSelector } from '@/context/BusterAppLayout';
|
|
|
|
export const TermIndividualContent: React.FC<{
|
|
termId: string;
|
|
}> = ({ termId }) => {
|
|
const updateTerm = useBusterTermsIndividualContextSelector((x) => x.updateTerm);
|
|
const { term: selectedTerm } = useBusterTermsIndividual({ termId });
|
|
const loadingSelectedTerm = !selectedTerm?.id;
|
|
|
|
const [editingTermName, setEditingTermName] = useState(false);
|
|
const [termName, setTermName] = useState(selectedTerm?.name || '');
|
|
const [termDefinition, setTermDefinition] = useState(selectedTerm?.definition || '');
|
|
const [termSQL, setTermSQL] = useState(selectedTerm?.sql_snippet || '');
|
|
const [sqlHeight, setSqlHeight] = useState(300);
|
|
|
|
const onSetTermName = (value: string) => {
|
|
setTermName(value);
|
|
updateTerm({
|
|
id: termId,
|
|
name: value
|
|
});
|
|
};
|
|
|
|
const onSetTermDefinition = (value: string) => {
|
|
setTermDefinition(value);
|
|
updateTerm({
|
|
id: termId,
|
|
definition: value
|
|
});
|
|
};
|
|
|
|
const onSetTermSQL = useDebounceFn(
|
|
(value: string) => {
|
|
setTermSQL(value);
|
|
updateTerm({
|
|
id: termId,
|
|
sql_snippet: value
|
|
});
|
|
},
|
|
{ wait: 500 }
|
|
);
|
|
|
|
useEffect(() => {
|
|
setTermName(selectedTerm?.name || '');
|
|
setTermDefinition(selectedTerm?.definition || '');
|
|
setTermSQL(selectedTerm?.sql_snippet || '');
|
|
}, [selectedTerm?.name, selectedTerm?.definition]);
|
|
|
|
return (
|
|
<AppContent className="overflow-auto p-8">
|
|
{loadingSelectedTerm ? (
|
|
<SkeletonLoader />
|
|
) : (
|
|
<div className="flex flex-col">
|
|
<div className="flex justify-between space-x-3">
|
|
<div className="mb-5 flex flex-col space-y-0.5">
|
|
<div className={'overflow-hidden'}>
|
|
<EditableTitle
|
|
editing={editingTermName}
|
|
onEdit={setEditingTermName}
|
|
onChange={(v) => {
|
|
onSetTermName(v);
|
|
}}
|
|
level={4}>
|
|
{termName}
|
|
</EditableTitle>
|
|
</div>
|
|
<div>
|
|
<Text type="secondary">
|
|
Last updated:{' '}
|
|
{formatDate({
|
|
date: selectedTerm?.updated_at!,
|
|
format: 'lll'
|
|
})}
|
|
</Text>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<MoreDropdown termId={termId} setEditingTermName={setEditingTermName} />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col space-y-4">
|
|
<ItemContainer title="Definition">
|
|
<div className={'overflow-hidden'}>
|
|
<Input.TextArea
|
|
key={selectedTerm?.id || 'default'}
|
|
defaultValue={selectedTerm?.definition || termDefinition}
|
|
autoSize={{ minRows: 3, maxRows: 20 }}
|
|
placeholder={'Enter definition...'}
|
|
onBlur={(e) => {
|
|
onSetTermDefinition(e.target.value);
|
|
}}
|
|
variant="borderless"
|
|
/>
|
|
</div>
|
|
</ItemContainer>
|
|
|
|
<ItemContainer
|
|
title={
|
|
<div className="flex w-full items-center justify-between space-x-2">
|
|
<Text>SQL Snippet</Text>
|
|
|
|
<AppMaterialIcons className="cursor-pointer" size={18} icon="help" />
|
|
</div>
|
|
}>
|
|
<div className="relative h-full w-full" style={{ height: sqlHeight }}>
|
|
<AppCodeEditor
|
|
style={{ minHeight: sqlHeight }}
|
|
defaultValue={termSQL}
|
|
onChangeEditorHeight={(v) => {
|
|
setSqlHeight(clamp(v, 300, 800));
|
|
}}
|
|
onChange={(v) => {
|
|
onSetTermSQL.run(v);
|
|
}}
|
|
monacoEditorOptions={{
|
|
scrollbar: {
|
|
alwaysConsumeMouseWheel: false
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
</ItemContainer>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</AppContent>
|
|
);
|
|
};
|
|
|
|
const SkeletonLoader: React.FC = () => {
|
|
return <div className="p-4">{/* <Skeleton /> */}</div>;
|
|
};
|
|
|
|
const MoreDropdown: React.FC<{ termId: string; setEditingTermName: (value: boolean) => void }> = ({
|
|
termId,
|
|
setEditingTermName
|
|
}) => {
|
|
const token = useAntToken();
|
|
const onDeleteTerm = useBusterTermsIndividualContextSelector((x) => x.onDeleteTerm);
|
|
const onChangePage = useAppLayoutContextSelector((s) => s.onChangePage);
|
|
|
|
const onDeleteTermsPreflight = async () => {
|
|
await onDeleteTerm({ ids: [termId] })
|
|
.then(() => {
|
|
onChangePage({
|
|
route: BusterRoutes.APP_TERMS
|
|
});
|
|
})
|
|
.catch((error) => {
|
|
//
|
|
});
|
|
};
|
|
|
|
const dropdownItems: MenuProps['items'] = useMemo(
|
|
() => [
|
|
{
|
|
key: 'edit',
|
|
icon: <AppMaterialIcons size={14} icon="edit" />,
|
|
label: 'Edit term title',
|
|
onClick: () => {
|
|
setEditingTermName(true);
|
|
}
|
|
},
|
|
{
|
|
key: 'delete',
|
|
icon: <AppMaterialIcons size={14} icon="delete" />,
|
|
label: 'Delete term',
|
|
onClick: onDeleteTermsPreflight
|
|
}
|
|
],
|
|
[setEditingTermName, onDeleteTermsPreflight]
|
|
);
|
|
|
|
const menu = useMemo(() => {
|
|
return {
|
|
items: dropdownItems
|
|
};
|
|
}, [dropdownItems]);
|
|
|
|
return (
|
|
<Dropdown trigger={['click']} menu={menu}>
|
|
<div
|
|
className="h-fit! cursor-pointer"
|
|
style={{
|
|
height: 18,
|
|
color: token.colorIcon
|
|
}}>
|
|
<AppMaterialIcons size={18} icon="more_horiz" />
|
|
</div>
|
|
</Dropdown>
|
|
);
|
|
};
|