Skip to content

Commit

Permalink
feat: support export and import instances (gh-187)
Browse files Browse the repository at this point in the history
Co-authored-by: Ricco X <ricco@riccox.com>
  • Loading branch information
cempehlivan and riccox authored Jan 16, 2025
1 parent f9ac8d3 commit 8a7e9c1
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 1 deletion.
127 changes: 127 additions & 0 deletions src/components/DashboardSettingsButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { useCallback, useRef } from 'react';
import { Instance, useAppStore } from '@/store';
import { useTranslation } from 'react-i18next';
import { IconFileExport, IconFileImport, IconSettings, IconTrash } from '@tabler/icons-react';
import { Menu, ActionIcon } from '@mantine/core';
import { Button } from '@douyinfe/semi-ui';
import { Modal } from '@arco-design/web-react';
import { cn } from '@/lib/cn';
import type { FC } from 'react';

interface Props {
className?: string;
}

export const DashboardSettingsButton: FC<Props> = ({ className = '' }) => {

const { t } = useTranslation('dashboard');
const instances = useAppStore((state) => state.instances);
const addInstance = useAppStore((state) => state.addInstance);
const removeAllInstances = useAppStore((state) => state.removeAllInstances);
const importInstancesFileInputRef = useRef<HTMLInputElement>(null);

const onClickExportInstances = () => {
if (instances.length > 0) {
const jsonString = JSON.stringify(instances, null, 2);
const blob = new Blob([jsonString], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `export-meilisearch-ui.json`;

document.body.appendChild(a);
a.click();

document.body.removeChild(a);
URL.revokeObjectURL(url);
}
}

const onClickImportInstances = () => {
if (importInstancesFileInputRef.current) {
importInstancesFileInputRef.current.click();
}
}

const handleImportInstancesFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;

const reader = new FileReader();
reader.onload = (e) => {
try {
const content = e.target?.result;
if (content) {
const parsed = JSON.parse(content as string);

if (isValidInstanceArray(parsed)) {
parsed.forEach(ins => {
const _ins = { ...ins, id: 0 };
addInstance(ins);
});
}
}
} catch (err: any) {
}
finally {
event.target.value = "";
}
};

reader.readAsText(file);
}

const isValidInstanceArray = (data: any): data is Instance[] => {
if (!Array.isArray(data)) return false;

return data.every(
(item) =>
typeof item.id === "number" &&
typeof item.name === "string" &&
typeof item.host === "string" &&
(item.apiKey === undefined || typeof item.apiKey === "string") &&
(item.updatedTime === undefined || !isNaN(new Date(item.updatedTime).getTime()))
);
}

const onClickRemoveAllInstances = useCallback(() => {
Modal.confirm({
title: t('settings.remove.title'),
centered: true,
content: t('settings.remove.tip'),
onOk: async () => {
return removeAllInstances();
},
okText: t('confirm'),
cancelText: t('cancel'),
});
}, [removeAllInstances, t]
)

return (
<div className={cn('px-2',className)}>
<input type="file" accept=".json" ref={importInstancesFileInputRef} style={{ display: 'none' }} onChange={handleImportInstancesFileUpload} id="import-instances" />
<Menu shadow="md" width={200}>
<Menu.Target>
<IconSettings className={'text-white w-5 h-5 cursor-pointer'} />
</Menu.Target>

<Menu.Dropdown>
<Menu.Item leftSection={<IconFileExport size={14} />} disabled={!(instances.length > 0)} onClick={onClickExportInstances}>
{t('settings.export')}
</Menu.Item>
<Menu.Item leftSection={<IconFileImport size={14} />} onClick={onClickImportInstances}>
{t('settings.import')}
</Menu.Item>

<Menu.Divider />

<Menu.Label>{t('settings.danger_zone')}</Menu.Label>
<Menu.Item leftSection={<IconTrash size={14} />} color="red" disabled={!(instances.length > 0)} onClick={onClickRemoveAllInstances}>
{t('settings.remove.title')}
</Menu.Item>
</Menu.Dropdown>
</Menu>
</div>
);
}
11 changes: 10 additions & 1 deletion src/locales/en/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,14 @@
"title": "Remove this instance",
"tip": "Are you sure you want to remove this instance"
}
},
"settings": {
"export": "Export instances",
"import": "Import instances",
"danger_zone": "Danger zone",
"remove": {
"title": "Remove all instances",
"tip": "Are you sure you want to remove all instances?"
}
}
}
}
9 changes: 9 additions & 0 deletions src/locales/zh/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,14 @@
"title": "移除这个实例",
"tip": "你真的希望移除这个实例"
}
},
"settings": {
"export": "导出实例",
"import": "导入实例",
"danger_zone": "危险区",
"remove": {
"title": "移除所有实例",
"tip": "您确实要删除所有实例吗?"
}
}
}
2 changes: 2 additions & 0 deletions src/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { createFileRoute, redirect } from '@tanstack/react-router';
import { Button } from '@douyinfe/semi-ui';
import { InsFormModal } from '@/components/instanceFormModal';
import { TimeAgo } from '@/components/timeago';
import { DashboardSettingsButton } from '@/components/DashboardSettingsButton';

const instanceCardClassName = `col-span-1 h-28 rounded-lg`;

Expand Down Expand Up @@ -162,6 +163,7 @@ function Dashboard() {
<div className={`w-1/2 2xl:w-1/4 h-2/3 flex flex-col justify-center items-center gap-y-10`}>
<Logo className="size-20" />
<p className={`text-primary-100 font-bold xl:text-3xl text-xl w-screen text-center`}>{t('slogan')}</p>
<DashboardSettingsButton className={'w-full flex justify-end -mb-8'} />
<div className={`grid grid-cols-1 gap-y-3 w-full p-1 overflow-y-scroll`}>
{instancesList}
<InsFormModal type="create">
Expand Down
7 changes: 7 additions & 0 deletions src/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ interface State {
addInstance: (cfg: Omit<Instance, 'updatedTime' | 'id'>) => void;
editInstance: (id: number, cfg: Omit<Instance, 'updatedTime' | 'id'>) => void;
removeInstance: (id: number) => void;
removeAllInstances: () => void;
}

export const useAppStore = create<State>()(
Expand Down Expand Up @@ -67,6 +68,12 @@ export const useAppStore = create<State>()(
if (index !== -1) state.instances.splice(index, 1);
})
),
removeAllInstances: () =>
set(
produce((state: State) => {
state.instances = [];
})
)
}),
{
name: 'meilisearch-ui-store',
Expand Down

0 comments on commit 8a7e9c1

Please sign in to comment.