This commit is contained in:
2024-12-07 19:20:04 +08:00
parent d84b92fe4f
commit 977492e78c
7 changed files with 3234 additions and 6757 deletions

View File

@@ -8,7 +8,7 @@
"build": "vite build",
"preview": "vite preview",
"typecheck": "tsc --noEmit",
"lint": "eslint src",
"lint": "eslint . --fix",
"jest": "jest",
"jest:watch": "jest --watch",
"test": "pnpm typecheck && pnpm lint",
@@ -17,91 +17,90 @@
"chromatic": "npx chromatic --project-token=180ac2186305"
},
"dependencies": {
"@mantine/core": "^7.6.2",
"@mantine/hooks": "^7.6.2",
"@mantine/notifications": "^7.6.2",
"@meilisearch/instant-meilisearch": "^0.18.1",
"@reduxjs/toolkit": "^2.2.1",
"@sentry/react": "^7.108.0",
"@sentry/vite-plugin": "^2.16.0",
"@tabler/icons-react": "^3.1.0",
"axios": "^1.6.8",
"dayjs": "^1.11.10",
"i18next": "^23.10.1",
"i18next-browser-languagedetector": "^7.2.0",
"instantsearch.js": "^4.71.1",
"@mantine/core": "^7.14.3",
"@mantine/hooks": "^7.14.3",
"@mantine/notifications": "^7.14.3",
"@meilisearch/instant-meilisearch": "^0.22.0",
"@reduxjs/toolkit": "^2.4.0",
"@sentry/react": "^8.42.0",
"@sentry/vite-plugin": "^2.22.7",
"@tabler/icons-react": "^3.24.0",
"axios": "^1.7.9",
"dayjs": "^1.11.13",
"i18next": "^24.0.5",
"i18next-browser-languagedetector": "^8.0.0",
"instantsearch.js": "^4.75.5",
"lodash": "^4.17.21",
"meilisearch": "^0.40.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^14.1.0",
"react-instantsearch": "^7.11.1",
"react-redux": "^9.1.0",
"react-router-dom": "^6.22.3",
"rehype-highlight": "^7.0.0",
"meilisearch": "^0.46.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^15.1.3",
"react-instantsearch": "^7.13.8",
"react-redux": "^9.1.2",
"react-router-dom": "^7.0.2",
"rehype-highlight": "^7.0.1",
"rehype-sanitize": "^6.0.0",
"rehype-stringify": "^10.0.0",
"rehype-stringify": "^10.0.1",
"remark-gfm": "^4.0.0",
"remark-html": "^16.0.1",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.0",
"unified": "^11.0.4"
"remark-rehype": "^11.1.1",
"unified": "^11.0.5"
},
"devDependencies": {
"@babel/core": "^7.24.1",
"@mantine/form": "^7.6.2",
"@storybook/addon-actions": "^8.0.2",
"@storybook/addon-essentials": "^8.0.2",
"@storybook/addon-interactions": "^8.0.2",
"@storybook/addon-links": "^8.0.2",
"@babel/core": "^7.26.0",
"@mantine/form": "^7.14.3",
"@storybook/addon-actions": "^8.4.7",
"@storybook/addon-essentials": "^8.4.7",
"@storybook/addon-interactions": "^8.4.7",
"@storybook/addon-links": "^8.4.7",
"@storybook/addons": "^7.6.17",
"@storybook/api": "^7.6.17",
"@storybook/builder-vite": "^8.0.2",
"@storybook/components": "^8.0.2",
"@storybook/core-events": "^8.0.2",
"@storybook/react": "^8.0.2",
"@storybook/testing-library": "^0.2.2",
"@storybook/theming": "^8.0.2",
"@testing-library/dom": "^9.3.4",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^14.2.2",
"@storybook/builder-vite": "^8.4.7",
"@storybook/components": "^8.4.7",
"@storybook/core-events": "^8.4.7",
"@storybook/react": "^8.4.7",
"@storybook/theming": "^8.4.7",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.1.0",
"@testing-library/user-event": "^14.5.2",
"@types/jest": "^29.5.12",
"@types/lodash": "^4.17.0",
"@types/react": "^18.2.67",
"@types/react-dom": "^18.2.22",
"@typescript-eslint/eslint-plugin": "^7.3.1",
"@typescript-eslint/parser": "^7.3.1",
"@vitejs/plugin-react": "^4.2.1",
"babel-loader": "^9.1.3",
"chromatic": "^11.1.1",
"eslint": "^8.57.0",
"@types/jest": "^29.5.14",
"@types/lodash": "^4.17.13",
"@types/react": "^18.3.1",
"@types/react-dom": "^18.3.1",
"@typescript-eslint/eslint-plugin": "^8.17.0",
"@typescript-eslint/parser": "^8.17.0",
"@vitejs/plugin-react": "^4.3.4",
"babel-loader": "^9.2.1",
"chromatic": "^11.20.0",
"eslint": "^9.16.0",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsx-a11y": "^6.8.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6",
"eslint-plugin-storybook": "^0.8.0",
"i18next-http-backend": "^2.5.0",
"eslint-import-resolver-typescript": "^3.7.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.16",
"eslint-plugin-storybook": "^0.11.1",
"i18next-http-backend": "^2.7.1",
"install-peerdeps": "^3.0.3",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"postcss": "^8.4.37",
"postcss-preset-mantine": "^1.13.0",
"postcss": "^8.4.49",
"postcss-preset-mantine": "^1.17.0",
"postcss-simple-vars": "^7.0.1",
"prettier": "^3.2.5",
"react-router": "^6.22.3",
"prettier": "^3.4.2",
"react-router": "^7.0.2",
"rollup-plugin-visualizer": "^5.12.0",
"storybook": "^8.0.2",
"storybook-dark-mode": "^4.0.1",
"storybook-react-i18next": "3.0.1",
"storybook": "^8.4.7",
"storybook-dark-mode": "^4.0.2",
"storybook-react-i18next": "3.1.7",
"stylis-plugin-rtl": "^2.1.1",
"ts-jest": "^29.1.2",
"typescript": "^5.4.2",
"vite": "^5.2.0"
"ts-jest": "^29.2.5",
"typescript": "^5.7.2",
"vite": "^6.0.3"
},
"packageManager": "pnpm@10.0.0-beta.1+sha512.629de0531b9ae9a3f8e372d014ef8f5a57906d9a48095ced54bbfbd246b4136381478032c8d13819fd1eedde8330517a799ea6756eedd9a136e36524fa3083cf"
}

9570
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -17,7 +17,10 @@ import {
useSortBy,
} from "react-instantsearch";
const sortItems = [{ value: "paragraph:time:desc", label: "Newest" }];
const sortItems = [
{ value: "paragraph:time:desc", label: "Newest" },
{ value: "paragraph:time:asc", label: "Oldest" },
];
const hitsPerPageItems = [
{ value: 20, label: "20", default: true },
{ value: 40, label: "40" },

View File

@@ -45,7 +45,6 @@ export default function MainLayout() {
{
finitePagination: true,
meiliSearchParams: {
hybrid: {},
attributesToRetrieve: [
"cover",
"title",
@@ -83,7 +82,7 @@ export default function MainLayout() {
</Group>
</Group>
</AppShell.Header>
<AppShell.Main pt={`calc(${rem(60)} + var(--mantine-spacing-md))`}>
<AppShell.Main pt={`calc(${rem(60)} + var(--mantine-spacing-md))`} pb={rem(60)}>
<Suspense fallback={<Loading />}>
<Outlet />
</Suspense>

View File

@@ -1,79 +1,167 @@
import { Container, Paper, Table, Text, TextInput, Title } from "@mantine/core";
import { ReactNode, useContext, useEffect } from "react";
import {
ActionIcon,
Button,
Container,
Paper,
PasswordInput,
rem,
Switch,
Table,
Text,
TextInput,
Title
} from "@mantine/core";
import { useForm } from "@mantine/form";
import { ReactNode, useContext, useEffect, useState } from "react";
import { TitleContext } from "@/component/Header/Header";
import { ThemeSetting } from "@/component/Settings/Theme";
import { disableSentry } from "@/sentry";
import store from "@/store";
import { useOptionsState } from "@/store/module/options";
import {
setMeilisearchToken,
setMeilisearchUrl,
setS3Url,
} from "@/store/reducer/options";
import { setMeilisearchToken, setMeilisearchUrl, setS3Url } from "@/store/reducer/options";
import { useLocalStorage } from "@mantine/hooks";
import { IconBrandGithub, IconMail } from "@tabler/icons-react";
import { Meilisearch } from "meilisearch";
interface SettingItem {
title: string;
description: string;
value: ReactNode;
title?: ReactNode;
description?: ReactNode;
value?: ReactNode;
}
export default function SettingsPage() {
const [_, setTitle] = useContext(TitleContext);
const { state: options } = useOptionsState();
const [sentryDisabled, setSentryDisabled] = useLocalStorage({
key: "sentryDisabled",
defaultValue: false,
});
useEffect(() => {
setTitle("Settings");
}, [setTitle]);
const settings: SettingItem[] = [
{
title: "Theme",
description: "Change the theme of your UI",
value: <ThemeSetting />,
},
{
title: "Minio URL",
description: "The URL of your Minio instance",
value: (
<TextInput
value={options.s3Url}
onChange={(e) => {
store.dispatch(setS3Url(e.currentTarget.value));
}}
/>
),
},
{
title: "Meilisearch URL",
description: "The URL of your Meilisearch instance",
value: (
<TextInput
value={options.meilisearchUrl}
onChange={(e) => {
store.dispatch(setMeilisearchUrl(e.currentTarget.value));
}}
/>
),
},
{
title: "Meilisearch Token",
description: "The token of your Meilisearch instance",
value: (
<TextInput
value={options.meilisearchToken}
onChange={(e) => {
store.dispatch(setMeilisearchToken(e.currentTarget.value));
}}
/>
),
},
{
title: "Made By",
description: "Yoshino-s",
value: (
<a href="https://github.com/yoshino-s">https://github.com/yoshino-s</a>
),
},
const form = useForm({
initialValues: options,
validateInputOnBlur: true,
validateInputOnChange: true,
});
const [meilisearchVersion, setMeilisearchVersion] = useState<string | null>(null);
const onSubmit = async (v: typeof options) => {
try {
new URL(v.s3Url, location.origin);
form.clearFieldError("s3Url");
} catch (e) {
form.setFieldError("s3Url", "Invalid Minio URL");
}
try {
const client = new Meilisearch({
host: v.meilisearchUrl,
apiKey: v.meilisearchToken,
});
const version = await client.getVersion();
setMeilisearchVersion(version.pkgVersion);
form.clearFieldError("meilisearchUrl");
form.clearFieldError("meilisearchToken");
} catch (e) {
form.setFieldError("meilisearchUrl", "Invalid Meilisearch URL");
form.setFieldError("meilisearchToken", "Invalid Meilisearch Token");
}
if (form.errors.length !== 0) {
return;
}
if (v.s3Url !== options.s3Url) {
store.dispatch(setS3Url(v.s3Url));
}
if (v.meilisearchUrl !== options.meilisearchUrl) {
store.dispatch(setMeilisearchUrl(v.meilisearchUrl));
}
if (v.meilisearchToken !== options.meilisearchToken) {
store.dispatch(setMeilisearchToken(v.meilisearchToken));
}
};
const settings: SettingItem[][] = [
[
{
title: "Theme",
description: "Change the theme of your UI",
value: <ThemeSetting />,
},
{
title: "Disable Sentry",
description: "Disable Sentry error reporting (not recommended)",
value: (
<div>
<Switch
size="md"
checked={sentryDisabled}
onChange={(value) => {
const v = value.currentTarget.checked;
setSentryDisabled(v);
disableSentry(v);
}}
/>
</div>
)
}
],
[
{
title: "Minio URL",
description: "The URL of your Minio instance",
value: <TextInput {...form.getInputProps("s3Url")} required />,
},
{
title: "Meilisearch URL",
description: "The URL of your Meilisearch instance",
value: <TextInput {...form.getInputProps("meilisearchUrl")} required />,
},
{
title: "Meilisearch Token",
description: "The token of your Meilisearch instance",
value: (
<PasswordInput {...form.getInputProps("meilisearchToken")} required />
),
},
{
title: (
<Button type="submit">Test And Save</Button>
),
value: (
meilisearchVersion ? (
<span style={{
color: "var(--mantine-color-green-6)",
}}>Meilisearch V{meilisearchVersion}</span>
) : null
)
}
],
[
{
title: "Made With ❤️ By",
description: (
<a href="https://github.com/yoshino-s">https://github.com/yoshino-s</a>
),
value: (
<ActionIcon.Group>
<ActionIcon variant="default" size="lg" aria-label="Gallery" component="a" href="https://github.com/yoshino-s" target="_blank">
<IconBrandGithub style={{ width: rem(20) }} stroke={1.5} />
</ActionIcon>
<ActionIcon variant="default" size="lg" aria-label="Settings" component="a" href="mailto:yoshino.prog@gmail.com">
<IconMail style={{ width: rem(20) }} stroke={1.5} />
</ActionIcon>
</ActionIcon.Group>
)
},
]
];
return (
@@ -82,31 +170,33 @@ export default function SettingsPage() {
Settings
</Title>
Customize the look and feel of your Coder deployment.
<Paper my="xl" radius="md" withBorder style={{ overflow: "hidden" }}>
<Table verticalSpacing="lg" striped>
<Table.Thead>
<Table.Tr>
<Table.Th>Name</Table.Th>
<Table.Th>Value</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{settings.map((setting) => (
<Table.Tr key={`${setting.title}`}>
<Table.Td>
<Text size="md" fw={500}>
{setting.title}
</Text>
<Text c="dimmed" size="sm">
{setting.description}
</Text>
</Table.Td>
<Table.Td>{setting.value}</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Paper>
<form onSubmit={form.onSubmit(onSubmit)}>
{
settings.map((settingItem, index) => (
<Paper my="xl" radius="md" withBorder style={{ overflow: "hidden" }} key={index}>
<Table verticalSpacing="lg" striped>
<Table.Tbody>
{settingItem.map((setting) => (
<Table.Tr key={`${setting.title}`}>
<Table.Td>
<Text size="md" fw={500}>
{setting.title}
</Text>
<Text c="dimmed" size="sm">
{setting.description}
</Text>
</Table.Td>
<Table.Td>{setting.value}</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Paper>
))
}
</form>
</Container>
);
}

View File

@@ -1,6 +1,6 @@
import * as Sentry from "@sentry/react";
Sentry.init({
export const client = Sentry.init({
dsn: "https://fee4fec58516c464f60613b40b5d3a7d@sentry.yoshino-s.xyz/2",
integrations: [
Sentry.browserProfilingIntegration(),
@@ -19,4 +19,13 @@ Sentry.init({
replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur.
profilesSampleRate: 1.0, // Capture 100% of the profiles
enabled: localStorage.getItem("sentryDisabled") !== "true",
});
export function disableSentry(disable: boolean) {
if (!client) {
return;
}
client.getOptions().enabled = !disable;
}

View File

@@ -6,15 +6,16 @@ import optionsReducer from "./reducer/options";
const localStorageMiddleware: Middleware = ({ getState }) => {
return (next) => (action) => {
const result = next(action);
console.log(result);
localStorage.setItem("applicationState", JSON.stringify(getState()));
const state = getState();
localStorage.setItem("applicationState", JSON.stringify(state));
return result;
};
};
const reHydrateStore = () => {
if (localStorage.getItem("applicationState") !== null) {
return JSON.parse(localStorage.getItem("applicationState") ?? "{}"); // re-hydrate the store
const state: any = JSON.parse(localStorage.getItem("applicationState") ?? "{}");
return state;// re-hydrate the store
}
};