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

View File

@@ -45,7 +45,6 @@ export default function MainLayout() {
{ {
finitePagination: true, finitePagination: true,
meiliSearchParams: { meiliSearchParams: {
hybrid: {},
attributesToRetrieve: [ attributesToRetrieve: [
"cover", "cover",
"title", "title",
@@ -83,7 +82,7 @@ export default function MainLayout() {
</Group> </Group>
</Group> </Group>
</AppShell.Header> </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 />}> <Suspense fallback={<Loading />}>
<Outlet /> <Outlet />
</Suspense> </Suspense>

View File

@@ -1,79 +1,167 @@
import { Container, Paper, Table, Text, TextInput, Title } from "@mantine/core"; import {
import { ReactNode, useContext, useEffect } from "react"; 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 { TitleContext } from "@/component/Header/Header";
import { ThemeSetting } from "@/component/Settings/Theme"; import { ThemeSetting } from "@/component/Settings/Theme";
import { disableSentry } from "@/sentry";
import store from "@/store"; import store from "@/store";
import { useOptionsState } from "@/store/module/options"; import { useOptionsState } from "@/store/module/options";
import { import { setMeilisearchToken, setMeilisearchUrl, setS3Url } from "@/store/reducer/options";
setMeilisearchToken, import { useLocalStorage } from "@mantine/hooks";
setMeilisearchUrl, import { IconBrandGithub, IconMail } from "@tabler/icons-react";
setS3Url, import { Meilisearch } from "meilisearch";
} from "@/store/reducer/options";
interface SettingItem { interface SettingItem {
title: string; title?: ReactNode;
description: string; description?: ReactNode;
value: ReactNode; value?: ReactNode;
} }
export default function SettingsPage() { export default function SettingsPage() {
const [_, setTitle] = useContext(TitleContext); const [_, setTitle] = useContext(TitleContext);
const { state: options } = useOptionsState(); const { state: options } = useOptionsState();
const [sentryDisabled, setSentryDisabled] = useLocalStorage({
key: "sentryDisabled",
defaultValue: false,
});
useEffect(() => { useEffect(() => {
setTitle("Settings"); setTitle("Settings");
}, [setTitle]); }, [setTitle]);
const settings: SettingItem[] = [ 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", title: "Theme",
description: "Change the theme of your UI", description: "Change the theme of your UI",
value: <ThemeSetting />, value: <ThemeSetting />,
}, },
{ {
title: "Minio URL", title: "Disable Sentry",
description: "The URL of your Minio instance", description: "Disable Sentry error reporting (not recommended)",
value: ( value: (
<TextInput <div>
value={options.s3Url} <Switch
onChange={(e) => { size="md"
store.dispatch(setS3Url(e.currentTarget.value)); 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", title: "Meilisearch URL",
description: "The URL of your Meilisearch instance", description: "The URL of your Meilisearch instance",
value: ( value: <TextInput {...form.getInputProps("meilisearchUrl")} required />,
<TextInput
value={options.meilisearchUrl}
onChange={(e) => {
store.dispatch(setMeilisearchUrl(e.currentTarget.value));
}}
/>
),
}, },
{ {
title: "Meilisearch Token", title: "Meilisearch Token",
description: "The token of your Meilisearch instance", description: "The token of your Meilisearch instance",
value: ( value: (
<TextInput <PasswordInput {...form.getInputProps("meilisearchToken")} required />
value={options.meilisearchToken}
onChange={(e) => {
store.dispatch(setMeilisearchToken(e.currentTarget.value));
}}
/>
), ),
}, },
{ {
title: "Made By", title: (
description: "Yoshino-s", <Button type="submit">Test And Save</Button>
),
value: ( 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> <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 ( return (
@@ -82,16 +170,14 @@ export default function SettingsPage() {
Settings Settings
</Title> </Title>
Customize the look and feel of your Coder deployment. Customize the look and feel of your Coder deployment.
<Paper my="xl" radius="md" withBorder style={{ overflow: "hidden" }}>
<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 verticalSpacing="lg" striped>
<Table.Thead>
<Table.Tr>
<Table.Th>Name</Table.Th>
<Table.Th>Value</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody> <Table.Tbody>
{settings.map((setting) => ( {settingItem.map((setting) => (
<Table.Tr key={`${setting.title}`}> <Table.Tr key={`${setting.title}`}>
<Table.Td> <Table.Td>
<Text size="md" fw={500}> <Text size="md" fw={500}>
@@ -107,6 +193,10 @@ export default function SettingsPage() {
</Table.Tbody> </Table.Tbody>
</Table> </Table>
</Paper> </Paper>
))
}
</form>
</Container> </Container>
); );
} }

View File

@@ -1,6 +1,6 @@
import * as Sentry from "@sentry/react"; import * as Sentry from "@sentry/react";
Sentry.init({ export const client = Sentry.init({
dsn: "https://fee4fec58516c464f60613b40b5d3a7d@sentry.yoshino-s.xyz/2", dsn: "https://fee4fec58516c464f60613b40b5d3a7d@sentry.yoshino-s.xyz/2",
integrations: [ integrations: [
Sentry.browserProfilingIntegration(), 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. 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 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 }) => { const localStorageMiddleware: Middleware = ({ getState }) => {
return (next) => (action) => { return (next) => (action) => {
const result = next(action); const result = next(action);
console.log(result); const state = getState();
localStorage.setItem("applicationState", JSON.stringify(getState())); localStorage.setItem("applicationState", JSON.stringify(state));
return result; return result;
}; };
}; };
const reHydrateStore = () => { const reHydrateStore = () => {
if (localStorage.getItem("applicationState") !== null) { 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
} }
}; };