mirror of
https://git.sr.ht/~ashkeel/strimertul
synced 2024-09-20 02:00:49 +00:00
Add stulbe page and feedback on saving
This commit is contained in:
parent
8bb2f79fd9
commit
2703c39df8
14 changed files with 398 additions and 94 deletions
|
@ -9,6 +9,7 @@ module.exports = {
|
|||
'no-shadow': 'off',
|
||||
'@typescript-eslint/no-use-before-define': ['error'],
|
||||
'@typescript-eslint/no-shadow': ['error'],
|
||||
'default-case': 'off',
|
||||
},
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
|
|
13
frontend/package-lock.json
generated
13
frontend/package-lock.json
generated
|
@ -922,6 +922,11 @@
|
|||
"readdirp": "~3.5.0"
|
||||
}
|
||||
},
|
||||
"clsx": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz",
|
||||
"integrity": "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA=="
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
|
@ -2520,6 +2525,14 @@
|
|||
"react-router": "6.1.1"
|
||||
}
|
||||
},
|
||||
"react-toastify": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-8.1.0.tgz",
|
||||
"integrity": "sha512-M+Q3rTmEw/53Csr7NsV/YnldJe4c7uERcY7Tma9mvLU98QT2VhIkKwjBzzxZkJRk/oBKyUAtkyMjMgO00hx6gQ==",
|
||||
"requires": {
|
||||
"clsx": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"read-cache": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
"react-i18next": "^11.12.0",
|
||||
"react-redux": "^7.2.4",
|
||||
"react-router-dom": "^6.1.1",
|
||||
"react-toastify": "^8.1.0",
|
||||
"redux-devtools-extension": "^2.13.9",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"sass": "^1.32.12",
|
||||
|
|
|
@ -5,6 +5,7 @@ import { BrowserRouter } from 'react-router-dom';
|
|||
|
||||
import 'inter-ui/inter.css';
|
||||
import 'normalize.css/normalize.css';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
|
||||
import './locale/setup';
|
||||
import './style.css';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { ActionCreatorWithOptionalPayload, AsyncThunk } from '@reduxjs/toolkit';
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import {
|
||||
KilovoltMessage,
|
||||
|
@ -9,6 +9,11 @@ import { RootState } from '../store';
|
|||
import apiReducer, { getUserPoints } from '../store/api/reducer';
|
||||
import { APIState, LoyaltyStorage, RequestStatus } from '../store/api/types';
|
||||
|
||||
interface LoadStatus {
|
||||
load: RequestStatus;
|
||||
save: RequestStatus;
|
||||
}
|
||||
|
||||
export function useModule<T>({
|
||||
key,
|
||||
selector,
|
||||
|
@ -27,7 +32,7 @@ export function useModule<T>({
|
|||
T,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
AsyncThunk<KilovoltMessage, T, {}>,
|
||||
{ load: RequestStatus; save: RequestStatus },
|
||||
LoadStatus,
|
||||
] {
|
||||
const client = useSelector((state: RootState) => state.api.client);
|
||||
const data = useSelector((state: RootState) => selector(state.api));
|
||||
|
@ -46,6 +51,9 @@ export function useModule<T>({
|
|||
client.subscribeKey(key, subscriber);
|
||||
return () => {
|
||||
client.unsubscribeKey(key, subscriber);
|
||||
dispatch(
|
||||
apiReducer.actions.requestKeysRemoved([`save-${key}`, `load-${key}`]),
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
return [
|
||||
|
@ -58,6 +66,25 @@ export function useModule<T>({
|
|||
];
|
||||
}
|
||||
|
||||
export function useStatus(
|
||||
status: RequestStatus | null,
|
||||
interval = 5000,
|
||||
): RequestStatus | null {
|
||||
const [localStatus, setlocalStatus] = useState(status);
|
||||
const maxTime = Date.now() - interval;
|
||||
useEffect(() => {
|
||||
const remaining = status?.updated.getTime() - maxTime;
|
||||
if (remaining) {
|
||||
setTimeout(() => {
|
||||
setlocalStatus(null);
|
||||
}, remaining);
|
||||
}
|
||||
setlocalStatus(status);
|
||||
}, [status]);
|
||||
|
||||
return status?.updated.getTime() > maxTime ? localStatus : null;
|
||||
}
|
||||
|
||||
export function useUserPoints(): LoyaltyStorage {
|
||||
const prefix = 'loyalty/points/';
|
||||
const client = useSelector((state: RootState) => state.api.client);
|
||||
|
@ -72,7 +99,7 @@ export function useUserPoints(): LoyaltyStorage {
|
|||
};
|
||||
client.subscribePrefix(prefix, subscriber);
|
||||
return () => {
|
||||
client.subscribePrefix(prefix, subscriber);
|
||||
client.unsubscribePrefix(prefix, subscriber);
|
||||
};
|
||||
}, []);
|
||||
return data;
|
||||
|
@ -80,5 +107,6 @@ export function useUserPoints(): LoyaltyStorage {
|
|||
|
||||
export default {
|
||||
useModule,
|
||||
useStatus,
|
||||
useUserPoints,
|
||||
};
|
||||
|
|
|
@ -37,11 +37,23 @@
|
|||
"bind-help": "Every application that uses strimertul will need to be updated, your browser will be redirected to the new URL automatically.",
|
||||
"static-path": "Static assets (leave empty to disable)",
|
||||
"static-placeholder": "Absolute path to static assets",
|
||||
"static-help": "Will be served at the following URL: {{url}}"
|
||||
"static-help": "Will be served at the following URL: {{url}}",
|
||||
"saving": "Saving webserver settings..."
|
||||
},
|
||||
"stulbe": {
|
||||
"title": "Back-end integration",
|
||||
"test-success": "Connection test was successful!",
|
||||
"test-button": "Test connection",
|
||||
"endpoint": "Back-end endpoint",
|
||||
"auth-key": "Authorization key",
|
||||
"username": "User name",
|
||||
"subtitle": "Optional back-end integration (using <1>stulbe</1> or any Kilovolt compatible endpoint) for syncing keys and obtaining webhook events",
|
||||
"bind-placeholder": "HTTP endpoint, leave empty to disable integration"
|
||||
}
|
||||
},
|
||||
"form-actions": {
|
||||
"save": "Save",
|
||||
"saving": "Saving...",
|
||||
"saved": "Saved",
|
||||
"error": "Error"
|
||||
},
|
||||
|
|
|
@ -283,6 +283,11 @@ const apiReducer = createSlice({
|
|||
) {
|
||||
state.loyalty.users[user] = entry;
|
||||
},
|
||||
requestKeysRemoved(state, { payload }: PayloadAction<string[]>) {
|
||||
payload.forEach((key) => {
|
||||
delete state.requestStatus[key];
|
||||
});
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(createWSClient.fulfilled, (state, { payload }) => {
|
||||
|
@ -294,25 +299,43 @@ const apiReducer = createSlice({
|
|||
});
|
||||
Object.values(modules).forEach((mod) => {
|
||||
builder.addCase(mod.getter.pending, (state) => {
|
||||
state.requestStatus[`load-${mod.key}`] = 'pending';
|
||||
state.requestStatus[`load-${mod.key}`] = {
|
||||
type: 'pending',
|
||||
updated: new Date(),
|
||||
};
|
||||
});
|
||||
builder.addCase(mod.getter.fulfilled, (state, action) => {
|
||||
state.requestStatus[`load-${mod.key}`] = 'success';
|
||||
state.requestStatus[`load-${mod.key}`] = {
|
||||
type: 'success',
|
||||
updated: new Date(),
|
||||
};
|
||||
mod.stateSetter(state, action);
|
||||
});
|
||||
builder.addCase(mod.getter.rejected, (state) => {
|
||||
state.requestStatus[`load-${mod.key}`] = 'error';
|
||||
// TODO Report error
|
||||
builder.addCase(mod.getter.rejected, (state, { error }) => {
|
||||
state.requestStatus[`load-${mod.key}`] = {
|
||||
type: 'error',
|
||||
error: error.message,
|
||||
updated: new Date(),
|
||||
};
|
||||
});
|
||||
builder.addCase(mod.setter.pending, (state) => {
|
||||
state.requestStatus[`save-${mod.key}`] = 'pending';
|
||||
state.requestStatus[`save-${mod.key}`] = {
|
||||
type: 'pending',
|
||||
updated: new Date(),
|
||||
};
|
||||
});
|
||||
builder.addCase(mod.setter.fulfilled, (state, action) => {
|
||||
state.requestStatus[`save-${mod.key}`] = 'success';
|
||||
builder.addCase(mod.setter.fulfilled, (state) => {
|
||||
state.requestStatus[`save-${mod.key}`] = {
|
||||
type: 'success',
|
||||
updated: new Date(),
|
||||
};
|
||||
});
|
||||
builder.addCase(mod.setter.rejected, (state) => {
|
||||
state.requestStatus[`save-${mod.key}`] = 'error';
|
||||
// TODO Report error
|
||||
builder.addCase(mod.setter.rejected, (state, { error }) => {
|
||||
state.requestStatus[`save-${mod.key}`] = {
|
||||
type: 'error',
|
||||
error: error.message,
|
||||
updated: new Date(),
|
||||
};
|
||||
});
|
||||
builder.addCase(mod.asyncSetter, mod.stateSetter);
|
||||
});
|
||||
|
|
|
@ -154,7 +154,10 @@ export enum ConnectionStatus {
|
|||
Connected,
|
||||
}
|
||||
|
||||
export type RequestStatus = 'pending' | 'success' | 'error';
|
||||
export type RequestStatus =
|
||||
| { type: 'pending'; updated: Date }
|
||||
| { type: 'success'; updated: Date }
|
||||
| { type: 'error'; updated: Date; error: string };
|
||||
|
||||
export interface APIState {
|
||||
client: KilovoltWS;
|
||||
|
|
|
@ -21,3 +21,8 @@ html {
|
|||
font-family: 'Inter var', 'system-ui';
|
||||
}
|
||||
}
|
||||
|
||||
a,
|
||||
a:visited {
|
||||
color: var(--teal11);
|
||||
}
|
||||
|
|
|
@ -12,6 +12,11 @@ import {
|
|||
TimerIcon,
|
||||
} from '@radix-ui/react-icons';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
|
||||
import Dashboard from './pages/Dashboard';
|
||||
import Sidebar, { RouteSection } from './components/Sidebar';
|
||||
import ServerSettingsPage from './pages/ServerSettings';
|
||||
import { RootState } from '../store';
|
||||
import { createWSClient } from '../store/api/reducer';
|
||||
import { ConnectionStatus } from '../store/api/types';
|
||||
|
@ -19,9 +24,7 @@ import { styled } from './theme';
|
|||
|
||||
// @ts-expect-error Asset import
|
||||
import spinner from '../assets/icon-loading.svg';
|
||||
import Dashboard from './pages/Dashboard';
|
||||
import Sidebar, { RouteSection } from './components/Sidebar';
|
||||
import ServerSettingsPage from './pages/ServerSettings';
|
||||
import BackendIntegrationPage from './pages/BackendIntegration';
|
||||
|
||||
function Loading() {
|
||||
const LoadingDiv = styled('div', {
|
||||
|
@ -74,7 +77,7 @@ const sections: RouteSection[] = [
|
|||
},
|
||||
{
|
||||
title: 'menu.pages.strimertul.stulbe',
|
||||
url: '/stulbe',
|
||||
url: '/backend',
|
||||
icon: <Link2Icon />,
|
||||
},
|
||||
],
|
||||
|
@ -167,7 +170,9 @@ export default function App(): JSX.Element {
|
|||
<Routes>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/http" element={<ServerSettingsPage />} />
|
||||
<Route path="/backend" element={<BackendIntegrationPage />} />
|
||||
</Routes>
|
||||
<ToastContainer position="bottom-center" autoClose={5000} theme="dark" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
|
34
frontend/src/ui/components/utils/SaveButton.tsx
Normal file
34
frontend/src/ui/components/utils/SaveButton.tsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { CheckIcon } from '@radix-ui/react-icons';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RequestStatus } from '../../../store/api/types';
|
||||
import { Button } from '../../theme';
|
||||
|
||||
interface SaveButtonProps {
|
||||
status: RequestStatus;
|
||||
}
|
||||
|
||||
function SaveButton(
|
||||
props: SaveButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
switch (props.status?.type) {
|
||||
case 'success':
|
||||
return (
|
||||
<Button variation="success" {...props}>
|
||||
{t('form-actions.saved')} <CheckIcon />
|
||||
</Button>
|
||||
);
|
||||
case 'error':
|
||||
return (
|
||||
<Button variation="error" {...props}>
|
||||
{t('form-actions.error')}
|
||||
</Button>
|
||||
);
|
||||
default:
|
||||
return <Button>{t('form-actions.save')}</Button>;
|
||||
}
|
||||
}
|
||||
|
||||
export default React.memo(SaveButton);
|
128
frontend/src/ui/pages/BackendIntegration.tsx
Normal file
128
frontend/src/ui/pages/BackendIntegration.tsx
Normal file
|
@ -0,0 +1,128 @@
|
|||
import React from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useModule, useStatus } from '../../lib/react-utils';
|
||||
import Stulbe from '../../lib/stulbe-lib';
|
||||
import apiReducer, { modules } from '../../store/api/reducer';
|
||||
import SaveButton from '../components/utils/SaveButton';
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Field,
|
||||
InputBox,
|
||||
Label,
|
||||
PageContainer,
|
||||
PageHeader,
|
||||
PageTitle,
|
||||
} from '../theme';
|
||||
|
||||
export default function BackendIntegrationPage(): React.ReactElement {
|
||||
const [stulbeConfig, setStulbeConfig, loadStatus] = useModule(
|
||||
modules.stulbeConfig,
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const status = useStatus(loadStatus.save);
|
||||
const active = stulbeConfig?.enabled ?? false;
|
||||
const busy =
|
||||
loadStatus.load?.type !== 'success' || loadStatus.save?.type === 'pending';
|
||||
|
||||
const test = async () => {
|
||||
try {
|
||||
const client = new Stulbe(stulbeConfig.endpoint);
|
||||
await client.auth(stulbeConfig.username, stulbeConfig.auth_key);
|
||||
toast.success(t('pages.stulbe.test-success'));
|
||||
} catch (e) {
|
||||
toast.error(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageHeader>
|
||||
<PageTitle>{t('pages.stulbe.title')}</PageTitle>
|
||||
<p>
|
||||
<Trans i18nKey="pages.stulbe.subtitle">
|
||||
{'Optional back-end integration (using '}
|
||||
<a href="https://github.com/strimertul/stulbe/">stulbe</a> or any
|
||||
Kilovolt compatible endpoint) for syncing keys and obtaining webhook
|
||||
events
|
||||
</Trans>
|
||||
</p>
|
||||
</PageHeader>
|
||||
<form
|
||||
onSubmit={(ev) => {
|
||||
dispatch(setStulbeConfig(stulbeConfig));
|
||||
ev.preventDefault();
|
||||
}}
|
||||
>
|
||||
<Field size="fullWidth">
|
||||
<Label htmlFor="endpoint">{t('pages.stulbe.endpoint')}</Label>
|
||||
<InputBox
|
||||
type="text"
|
||||
id="endpoint"
|
||||
placeholder={t('pages.stulbe.bind-placeholder')}
|
||||
value={stulbeConfig?.endpoint ?? ''}
|
||||
disabled={busy}
|
||||
onChange={(e) =>
|
||||
dispatch(
|
||||
apiReducer.actions.stulbeConfigChanged({
|
||||
...stulbeConfig,
|
||||
enabled: e.target.value.length > 0,
|
||||
endpoint: e.target.value,
|
||||
}),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<Field size="fullWidth">
|
||||
<Label htmlFor="username">{t('pages.stulbe.username')}</Label>
|
||||
<InputBox
|
||||
type="text"
|
||||
id="username"
|
||||
value={stulbeConfig?.username ?? ''}
|
||||
required={true}
|
||||
disabled={!active || busy}
|
||||
onChange={(e) =>
|
||||
dispatch(
|
||||
apiReducer.actions.stulbeConfigChanged({
|
||||
...stulbeConfig,
|
||||
username: e.target.value,
|
||||
}),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<Field size="fullWidth">
|
||||
<Label htmlFor="password">{t('pages.stulbe.auth-key')}</Label>
|
||||
<InputBox
|
||||
type="password"
|
||||
id="password"
|
||||
value={stulbeConfig?.auth_key ?? ''}
|
||||
disabled={!active || busy}
|
||||
required={true}
|
||||
onChange={(e) =>
|
||||
dispatch(
|
||||
apiReducer.actions.stulbeConfigChanged({
|
||||
...stulbeConfig,
|
||||
auth_key: e.target.value,
|
||||
}),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<ButtonGroup>
|
||||
<SaveButton status={status} />
|
||||
<Button
|
||||
type="button"
|
||||
disabled={!active || busy}
|
||||
onClick={() => test()}
|
||||
>
|
||||
{t('pages.stulbe.test-button')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</form>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useModule } from '../../lib/react-utils';
|
||||
import { useModule, useStatus } from '../../lib/react-utils';
|
||||
import apiReducer, { modules } from '../../store/api/reducer';
|
||||
import SaveButton from '../components/utils/SaveButton';
|
||||
import {
|
||||
Button,
|
||||
Field,
|
||||
FieldNote,
|
||||
InputBox,
|
||||
|
@ -20,83 +20,90 @@ export default function ServerSettingsPage(): React.ReactElement {
|
|||
);
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const busy = loadStatus.load !== 'success' || loadStatus.save === 'pending';
|
||||
const status = useStatus(loadStatus.save);
|
||||
const busy =
|
||||
loadStatus.load?.type !== 'success' || loadStatus.save?.type === 'pending';
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageHeader>
|
||||
<PageTitle>{t('pages.http.title')}</PageTitle>
|
||||
</PageHeader>
|
||||
<Field size="fullWidth">
|
||||
<Label htmlFor="bind">{t('pages.http.bind')}</Label>
|
||||
<InputBox
|
||||
type="text"
|
||||
id="bind"
|
||||
placeholder={t('pages.http.bind-placeholder')}
|
||||
value={serverConfig?.bind ?? ''}
|
||||
disabled={busy}
|
||||
onChange={(e) =>
|
||||
dispatch(
|
||||
apiReducer.actions.httpConfigChanged({
|
||||
...serverConfig,
|
||||
bind: e.target.value,
|
||||
}),
|
||||
)
|
||||
}
|
||||
/>
|
||||
<FieldNote>{t('pages.http.bind-help')}</FieldNote>
|
||||
</Field>
|
||||
<Field size="fullWidth">
|
||||
<Label htmlFor="kvpassword">{t('pages.http.kilovolt-password')}</Label>
|
||||
<InputBox
|
||||
type="password"
|
||||
id="kvpassword"
|
||||
placeholder={t('pages.http.kilovolt-placeholder')}
|
||||
value={serverConfig?.kv_password ?? ''}
|
||||
disabled={busy}
|
||||
onChange={(e) =>
|
||||
dispatch(
|
||||
apiReducer.actions.httpConfigChanged({
|
||||
...serverConfig,
|
||||
kv_password: e.target.value,
|
||||
}),
|
||||
)
|
||||
}
|
||||
/>
|
||||
<FieldNote>{t('pages.http.kilovolt-placeholder')}</FieldNote>
|
||||
</Field>
|
||||
<Field size="fullWidth">
|
||||
<Label htmlFor="static">{t('pages.http.static-path')}</Label>
|
||||
<InputBox
|
||||
type="text"
|
||||
id="static"
|
||||
placeholder={t('pages.http.static-placeholder')}
|
||||
disabled={busy}
|
||||
onChange={(e) =>
|
||||
dispatch(
|
||||
apiReducer.actions.httpConfigChanged({
|
||||
...serverConfig,
|
||||
path: e.target.value,
|
||||
}),
|
||||
)
|
||||
}
|
||||
value={
|
||||
serverConfig?.enable_static_server ? serverConfig?.path ?? '' : ''
|
||||
}
|
||||
/>
|
||||
<FieldNote>
|
||||
{t('pages.http.static-help', {
|
||||
url: `http://${serverConfig?.bind ?? 'localhost:4337'}/static/`,
|
||||
})}
|
||||
</FieldNote>
|
||||
</Field>
|
||||
<Button
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onClick={() => dispatch(setServerConfig(serverConfig))}
|
||||
<form
|
||||
onSubmit={(ev) => {
|
||||
dispatch(setServerConfig(serverConfig));
|
||||
ev.preventDefault();
|
||||
}}
|
||||
>
|
||||
{t('form-actions.save')}
|
||||
</Button>
|
||||
<Field size="fullWidth">
|
||||
<Label htmlFor="bind">{t('pages.http.bind')}</Label>
|
||||
<InputBox
|
||||
type="text"
|
||||
id="bind"
|
||||
placeholder={t('pages.http.bind-placeholder')}
|
||||
value={serverConfig?.bind ?? ''}
|
||||
disabled={busy}
|
||||
required={true}
|
||||
onChange={(e) =>
|
||||
dispatch(
|
||||
apiReducer.actions.httpConfigChanged({
|
||||
...serverConfig,
|
||||
bind: e.target.value,
|
||||
}),
|
||||
)
|
||||
}
|
||||
/>
|
||||
<FieldNote>{t('pages.http.bind-help')}</FieldNote>
|
||||
</Field>
|
||||
<Field size="fullWidth">
|
||||
<Label htmlFor="kvpassword">
|
||||
{t('pages.http.kilovolt-password')}
|
||||
</Label>
|
||||
<InputBox
|
||||
type="password"
|
||||
id="kvpassword"
|
||||
placeholder={t('pages.http.kilovolt-placeholder')}
|
||||
value={serverConfig?.kv_password ?? ''}
|
||||
disabled={busy}
|
||||
autoComplete="off"
|
||||
onChange={(e) =>
|
||||
dispatch(
|
||||
apiReducer.actions.httpConfigChanged({
|
||||
...serverConfig,
|
||||
kv_password: e.target.value,
|
||||
}),
|
||||
)
|
||||
}
|
||||
/>
|
||||
<FieldNote>{t('pages.http.kilovolt-placeholder')}</FieldNote>
|
||||
</Field>
|
||||
<Field size="fullWidth">
|
||||
<Label htmlFor="static">{t('pages.http.static-path')}</Label>
|
||||
<InputBox
|
||||
type="text"
|
||||
id="static"
|
||||
placeholder={t('pages.http.static-placeholder')}
|
||||
disabled={busy}
|
||||
onChange={(e) =>
|
||||
dispatch(
|
||||
apiReducer.actions.httpConfigChanged({
|
||||
...serverConfig,
|
||||
path: e.target.value,
|
||||
}),
|
||||
)
|
||||
}
|
||||
value={
|
||||
serverConfig?.enable_static_server ? serverConfig?.path ?? '' : ''
|
||||
}
|
||||
/>
|
||||
<FieldNote>
|
||||
{t('pages.http.static-help', {
|
||||
url: `http://${serverConfig?.bind ?? 'localhost:4337'}/static/`,
|
||||
})}
|
||||
</FieldNote>
|
||||
</Field>
|
||||
<SaveButton type="submit" status={status} />
|
||||
</form>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
import { grayDark, tealDark, yellowDark } from '@radix-ui/colors';
|
||||
import {
|
||||
grassDark,
|
||||
grayDark,
|
||||
redDark,
|
||||
tealDark,
|
||||
yellowDark,
|
||||
} from '@radix-ui/colors';
|
||||
import { createStitches } from '@stitches/react';
|
||||
import * as UnstyledLabel from '@radix-ui/react-label';
|
||||
|
||||
|
@ -8,6 +14,8 @@ export const { styled, theme } = createStitches({
|
|||
...grayDark,
|
||||
...tealDark,
|
||||
...yellowDark,
|
||||
...grassDark,
|
||||
...redDark,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -68,6 +76,16 @@ export const InputBox = styled('input', {
|
|||
borderColor: '$teal7',
|
||||
backgroundColor: '$gray3',
|
||||
},
|
||||
'&:disabled': {
|
||||
backgroundColor: '$gray4',
|
||||
borderColor: '$gray5',
|
||||
color: '$gray8',
|
||||
},
|
||||
});
|
||||
|
||||
export const ButtonGroup = styled('div', {
|
||||
display: 'flex',
|
||||
gap: '0.5rem',
|
||||
});
|
||||
|
||||
export const Button = styled('button', {
|
||||
|
@ -85,6 +103,31 @@ export const Button = styled('button', {
|
|||
'&:active': {
|
||||
background: '$teal6',
|
||||
},
|
||||
transition: 'all 0.2s',
|
||||
variants: {
|
||||
variation: {
|
||||
success: {
|
||||
border: '1px solid $grass6',
|
||||
backgroundColor: '$grass4',
|
||||
'&:hover': {
|
||||
backgroundColor: '$grass5',
|
||||
},
|
||||
'&:active': {
|
||||
background: '$grass6',
|
||||
},
|
||||
},
|
||||
error: {
|
||||
border: '1px solid $red6',
|
||||
backgroundColor: '$red4',
|
||||
'&:hover': {
|
||||
backgroundColor: '$red5',
|
||||
},
|
||||
'&:active': {
|
||||
background: '$red6',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default { styled, theme };
|
||||
|
|
Loading…
Reference in a new issue