Files
vscode-front-matter/src/dashboardWebView/components/DataView/DataView.tsx
2024-10-08 09:31:40 +02:00

408 lines
16 KiB
TypeScript

import * as React from 'react';
import { Header } from '../Header';
import { useRecoilValue } from 'recoil';
import { SettingsSelector } from '../../state';
import { DataForm } from './DataForm';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { DataFile } from '../../../models/DataFile';
import { messageHandler, Messenger } from '@estruyf/vscode/dist/client';
import { DashboardMessage } from '../../DashboardMessage';
import { SponsorMsg } from '../Layout/SponsorMsg';
import { EventData } from '@estruyf/vscode';
import { DashboardCommand } from '../../DashboardCommand';
import { arrayMoveImmutable } from 'array-move';
import { EmptyView } from './EmptyView';
import { Container } from './SortableContainer';
import { SortableItem } from './SortableItem';
import { ChevronRightIcon, CircleStackIcon, EyeIcon, XMarkIcon } from '@heroicons/react/24/outline';
import { DataType } from '../../../models/DataType';
import { GeneralCommands, WEBSITE_LINKS } from '../../../constants';
import { NavigationItem } from '../Layout';
import { LocalizationKey, localize } from '../../../localization';
import { DropdownMenu, DropdownMenuContent } from '../../../components/shadcn/Dropdown';
import { MenuButton, MenuItem } from '../Menu';
import { Transition } from '@headlessui/react';
import { DataFolder } from '../../../models';
import { ActionsBarItem } from '../Header/ActionsBarItem';
import { Spinner } from '../Common/Spinner';
import { openFile } from '../../utils/MessageHandlers';
import { Button } from 'vscrui';
export interface IDataViewProps { }
export const DataView: React.FunctionComponent<IDataViewProps> = () => {
const [selectedData, setSelectedData] = useState<DataFile | null>(null);
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
const [dataEntries, setDataEntries] = useState<any | any[] | null>(null);
const [loading, setLoading] = useState<boolean>(false);
const settings = useRecoilValue(SettingsSelector);
const setSchema = (dataFile: DataFile) => {
setSelectedData(dataFile);
setSelectedIndex(null);
setDataEntries(null);
Messenger.send(DashboardMessage.getDataEntries, { ...dataFile });
};
const messageListener = (message: MessageEvent<EventData<any>>) => {
if (message.data.command === DashboardCommand.dataFileEntries) {
setDataEntries(message.data.payload);
}
};
const deleteItem = useCallback(
(index: number) => {
const dataClone: any[] = Object.assign([], dataEntries);
if (!selectedData) {
return;
}
dataClone.splice(index, 1);
updateData(dataClone);
},
[selectedData, dataEntries]
);
const onSubmit = useCallback(
(data: unknown) => {
if (selectedData?.singleEntry) {
// Needs to add a single entry
updateData(data);
return;
}
const dataClone: unknown[] = Object.assign([], dataEntries);
if (selectedIndex !== null && selectedIndex !== undefined) {
dataClone[selectedIndex] = data;
} else {
dataClone.push(data);
}
updateData(dataClone);
},
[selectedData, dataEntries, selectedIndex]
);
const onSortEnd = useCallback(
({ oldIndex, newIndex }: any) => {
if (!dataEntries || dataEntries.length === 0) {
return null;
}
if (selectedIndex !== null && selectedIndex !== undefined) {
setSelectedIndex(newIndex);
}
const newEntries = arrayMoveImmutable(dataEntries, oldIndex, newIndex);
updateData(newEntries);
},
[selectedData, dataEntries, selectedIndex]
);
const updateData = useCallback(
(data: any) => {
if (!selectedData) {
return;
}
Messenger.send(DashboardMessage.putDataEntries, {
file: selectedData.file,
fileType: selectedData.fileType,
entries: data
});
Messenger.send(DashboardMessage.showNotification, localize(LocalizationKey.dashboardDataViewDataViewUpdateMessage));
},
[selectedData]
);
const createDataFile = (folder: DataFolder) => {
setLoading(true);
messageHandler.request<DataFile>(DashboardMessage.createDataFile, folder).then(dataFile => {
if (dataFile) {
setSchema(dataFile);
}
setLoading(false);
}).catch((_: any) => {
setLoading(false);
});
}
const dataEntry = useMemo(() => {
if (selectedData?.singleEntry) {
return dataEntries || {};
}
return dataEntries && selectedIndex !== null && selectedIndex !== undefined
? dataEntries[selectedIndex]
: null;
}, [selectedData, dataEntries, selectedIndex]);
// Retrieve the data files, check if they have a schema or ID, if not, they shouldn't be shown
const dataFiles = useMemo(() => {
return (settings?.dataFiles || [])
.map((dataFile: DataFile) => {
if (!dataFile.schema && !dataFile.id) {
return null;
}
const clonedFile = Object.assign({}, dataFile);
if (clonedFile.type) {
const dataType = settings?.dataTypes?.find(
(dataType: DataType) => dataType.id === clonedFile.type
);
if (!dataType) {
return null;
}
clonedFile.schema = Object.assign({}, dataType.schema);
}
return clonedFile;
})
.filter((d) => d !== null) as DataFile[];
}, [settings?.dataFiles]);
const fileCreationFolders = useMemo(() => {
return (settings?.dataFolders || [])
.filter((folder) => folder.enableFileCreation);
}, [settings?.dataFolders]);
const hasOnlyDataCreationFolders = useMemo(() => (!dataFiles || dataFiles.length === 0) && (fileCreationFolders && fileCreationFolders.length > 0), [dataFiles, fileCreationFolders]);
useEffect(() => {
Messenger.listen(messageListener);
Messenger.send(DashboardMessage.setTitle, localize(LocalizationKey.dashboardHeaderTabsData));
Messenger.send(GeneralCommands.toVSCode.logging.info, {
message: 'Data view loaded',
location: 'DASHBOARD'
});
return () => {
Messenger.unlisten(messageListener);
};
}, []);
return (
<div className="flex flex-col h-full overflow-auto inset-y-0">
<Header settings={settings} />
{(dataFiles && dataFiles.length > 0) || (fileCreationFolders && fileCreationFolders.length > 0) ? (
<div className={`relative w-full flex-grow mx-auto overflow-hidden`}>
{
!selectedData && (dataFiles && dataFiles.length > 0) && (
<div className={`flex w-64 flex-col absolute inset-y-0`}>
<aside
className={`flex flex-col flex-grow overflow-y-auto border-r py-6 px-4 overflow-auto border-[var(--frontmatter-border)]`}
>
<h2 className={`text-lg text-[var(--frontmatter-text)]`}>
{localize(LocalizationKey.dashboardDataViewDataViewSelect)}
</h2>
<nav className={`flex-1 py-4 -mx-4`}>
<div
className={`divide-y border-t border-b divide-[var(--frontmatter-border)] border-[var(--frontmatter-border)]`}
>
{dataFiles &&
dataFiles.length > 0 &&
dataFiles.map((dataFile, idx) => (
<NavigationItem
key={`${dataFile.id}-${idx}`}
onClick={() => setSchema(dataFile)}
>
<ChevronRightIcon className="-ml-1 w-5 mr-2" />
<span>{dataFile.title}</span>
</NavigationItem>
))}
</div>
</nav>
</aside>
</div>
)
}
<Transition
as={`div`}
show={!!selectedData}
enter="transition ease transform"
enterFrom="opacity-0"
enterTo="opacity-100">
<div className={`w-full px-4 py-2 border-b border-[var(--frontmatter-border)]`}>
<div className={`flex justify-between`}>
<div className={`flex gap-4`}>
{
selectedData && (
<DropdownMenu>
<MenuButton
label={localize(LocalizationKey.dashboardDataViewDataViewSelect)}
title={selectedData.title}
/>
<DropdownMenuContent>
{dataFiles.map((dataFile) => (
<MenuItem
key={dataFile.id}
title={dataFile.title}
value={dataFile}
isCurrent={selectedData.id === dataFile.id}
onClick={() => setSchema(dataFile)}
/>
))}
</DropdownMenuContent>
</DropdownMenu>
)
}
{
fileCreationFolders && fileCreationFolders.length > 0 && (
<DropdownMenu>
<MenuButton
label={localize(LocalizationKey.dashboardDataViewDataViewCreateNew)}
title={localize(LocalizationKey.dashboardDataViewDataViewSelectDataFolder)}
/>
<DropdownMenuContent>
{fileCreationFolders.map((folder) => (
<MenuItem
key={folder.id}
title={folder.id}
value={folder}
onClick={() => createDataFile(folder)}
/>
))}
</DropdownMenuContent>
</DropdownMenu>
)
}
</div>
{
selectedData && (
<div className={`flex gap-2`}>
<ActionsBarItem
className='flex items-center'
onClick={() => openFile(selectedData.file)}
title={localize(LocalizationKey.commonView)}
>
<EyeIcon className="w-4 h-4 mr-1" aria-hidden="true" />
<span>{localize(LocalizationKey.commonView)}</span>
</ActionsBarItem>
<ActionsBarItem
className='flex items-center hover:text-[var(--vscode-statusBarItem-warningBackground)]'
onClick={() => setSelectedData(null)}
title={localize(LocalizationKey.dashboardDataViewDataViewCloseSelectedDataFile)}
>
<XMarkIcon className="w-4 h-4 mr-1" aria-hidden="true" />
<span>{localize(LocalizationKey.dashboardDataViewDataViewCloseSelectedDataFile)}</span>
</ActionsBarItem>
</div>
)
}
</div>
</div>
</Transition>
<section className={`flex min-w-0 h-full ease transition-[padding] ${selectedData ? "" : hasOnlyDataCreationFolders ? "" : "pl-64"}`}>
{selectedData ? (
<>
{!selectedData.singleEntry && (
<div
className={`w-1/3 py-6 px-4 flex-1 border-r overflow-auto border-[var(--frontmatter-border)]`}
>
<h2 className={`text-lg text-[var(--frontmatter-text)]`}>
{localize(LocalizationKey.dashboardDataViewDataViewTitle, selectedData?.title?.toLowerCase() || '')}
</h2>
<div className="py-4">
{dataEntries && dataEntries.length > 0 ? (
<>
<Container onSortEnd={onSortEnd} useDragHandle>
{((dataEntries as any[]) || []).map((dataEntry, idx) => (
<SortableItem
key={dataEntry[selectedData.labelField] || `entry-${idx}`}
value={dataEntry[selectedData.labelField] || `Entry ${idx + 1}`}
index={idx}
crntIndex={idx}
selectedIndex={selectedIndex}
onSelectedIndexChange={(index: number) => setSelectedIndex(index)}
onDeleteItem={deleteItem}
/>
))}
</Container>
<Button className="mt-4 !py-2" onClick={() => setSelectedIndex(null)}>
{localize(LocalizationKey.dashboardDataViewDataViewAdd)}
</Button>
</>
) : (
<div className={`flex flex-col items-center justify-center`}>
<p className={`text-[var(--frontmatter-text)]`}>
{localize(LocalizationKey.dashboardDataViewDataViewEmpty, selectedData.title.toLowerCase())}
</p>
</div>
)}
</div>
</div>
)}
<div
className={`${selectedData.singleEntry ? 'w-full' : 'w-2/3'
} py-6 px-4 overflow-auto`}
>
<h2 className={`text-lg text-[var(--frontmatter-text)]`}>
{localize(LocalizationKey.dashboardDataViewDataViewCreateOrModify, selectedData.title.toLowerCase())}
</h2>
{selectedData ? (
<DataForm
schema={selectedData?.schema}
model={dataEntry}
onSubmit={onSubmit}
onClear={() => setSelectedIndex(null)}
/>
) : (
<p>{localize(LocalizationKey.dashboardDataViewDataViewGetStarted)}</p>
)}
</div>
</>
) : (
<EmptyView
folders={fileCreationFolders}
onCreate={createDataFile} />
)}
</section>
</div>
) : (
<div className="w-full h-full flex items-center justify-center">
<div className={`flex flex-col items-center text-[var(--frontmatter-text)]`}>
<CircleStackIcon className="w-32 h-32" />
<p className="text-3xl mt-2">{localize(LocalizationKey.dashboardDataViewDataViewNoDataFiles)}</p>
<p className="text-xl mt-4">
<a
className={`text-[var(--frontmatter-link)] hover:text-[var(--frontmatter-link-hover)]`}
href={WEBSITE_LINKS.docs.dataDashboard}
title={localize(LocalizationKey.dashboardDataViewDataViewGetStartedLink)}
>
{localize(LocalizationKey.dashboardDataViewDataViewGetStartedLink)}
</a>
</p>
</div>
</div>
)
}
{loading && <Spinner />}
<SponsorMsg
beta={settings?.beta}
version={settings?.versionInfo}
isBacker={settings?.isBacker}
/>
<img className='hidden' src="https://api.visitorbadge.io/api/visitors?path=https%3A%2F%2Ffrontmatter.codes%2Fmetrics%2Fdashboards&slug=DataView" alt="DataView metrics" />
</div >
);
};