mirror of
https://github.com/itplr-kosit/validator.git
synced 2026-05-25 16:55:39 +00:00
Resolve https://projekte.kosit.org/kosit/validator/-/issues/97 "Replace docsify from UI"
This commit is contained in:
parent
a10cc14d06
commit
219aeaa1b7
100 changed files with 27369 additions and 1072 deletions
105
server/ui/src/components/Button/Button.module.css
Normal file
105
server/ui/src/components/Button/Button.module.css
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
:where(.button) {
|
||||
--button-shadow: var(--ifm-global-shadow-lw);
|
||||
--button-accent-shadow: var(--ifm-global-shadow-md);
|
||||
--button-text-color: var(--text-accent-bg-0);
|
||||
--button-background-color: var(--surface-accent-3);
|
||||
--button-background-color-hover: var(--surface-accent-4);
|
||||
--button-background-color-disabled: var(--surface-4);
|
||||
--button-accent-shadow-opacity: 0;
|
||||
}
|
||||
|
||||
:where([data-theme="dark"] .button) {
|
||||
--button-shadow: none;
|
||||
--button-accent-shadow: none;
|
||||
--button-text-color: var(--text-accent-bg-0);
|
||||
--button-background-color: var(--surface-accent-4);
|
||||
--button-background-color-hover: var(--surface-accent-3);
|
||||
--button-background-color-disabled: var(--surface-5);
|
||||
--button-accent-shadow-opacity: 0;
|
||||
}
|
||||
|
||||
.button {
|
||||
position: relative;
|
||||
background: var(--button-background-color);
|
||||
font-family: inherit;
|
||||
font-size: 1rem;
|
||||
font-size: 0.875rem;
|
||||
text-transform: uppercase;
|
||||
color: var(--button-text-color);
|
||||
font-weight: var(--ifm-font-weight-semibold);
|
||||
border: none;
|
||||
padding: 0 1.25em;
|
||||
height: 2.25em;
|
||||
line-height: 1;
|
||||
border-radius: var(--border-radius-small);
|
||||
box-shadow: var(--button-shadow);
|
||||
cursor: pointer;
|
||||
transition: color 150ms ease, background-color 150ms ease;
|
||||
}
|
||||
|
||||
.button::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
box-shadow: var(--button-accent-shadow);
|
||||
opacity: var(--button-accent-shadow-opacity);
|
||||
transition: opacity 200ms ease;
|
||||
}
|
||||
|
||||
.button:where(:hover, :focus) {
|
||||
--button-background-color: var(--button-background-color-hover);
|
||||
}
|
||||
.button:not([disabled]):where(:hover, :focus) {
|
||||
--button-accent-shadow-opacity: 1;
|
||||
}
|
||||
.button:not([disabled]):where(:active) {
|
||||
--button-accent-shadow-opacity: 0.5;
|
||||
}
|
||||
|
||||
.button[disabled] {
|
||||
--button-background-color: var(--button-background-color-disabled);
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
.spinnerWrapper {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
background: #ffffff55;
|
||||
backdrop-filter: blur(1px);
|
||||
transition: opacity 150ms ease;
|
||||
}
|
||||
|
||||
.loading .spinnerWrapper {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
--_size: 1.75rem;
|
||||
--_thickness: 3px;
|
||||
|
||||
width: var(--_size);
|
||||
height: var(--_size);
|
||||
border: var(--_thickness) solid var(--button-text-color);
|
||||
border-bottom-color: transparent;
|
||||
border-radius: 50%;
|
||||
animation: spin 1100ms infinite cubic-bezier(0.5, 0.1, 0.5, 0.9);
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
rotate: 0deg;
|
||||
}
|
||||
100% {
|
||||
rotate: 360deg;
|
||||
}
|
||||
}
|
||||
44
server/ui/src/components/Button/Button.tsx
Normal file
44
server/ui/src/components/Button/Button.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import clsx from "clsx";
|
||||
import type { ButtonHTMLAttributes, DetailedHTMLProps, ReactNode } from "react";
|
||||
import React from "react";
|
||||
import type { ExtendProps } from "../util/types";
|
||||
import styles from "./Button.module.css";
|
||||
|
||||
type HTMLButtonProps = DetailedHTMLProps<
|
||||
ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
HTMLButtonElement
|
||||
>;
|
||||
type ButtonProps = ExtendProps<
|
||||
HTMLButtonProps,
|
||||
{
|
||||
children: ReactNode;
|
||||
type?: "button" | "submit" | "reset";
|
||||
className?: string;
|
||||
loading?: boolean;
|
||||
}
|
||||
>;
|
||||
|
||||
function Button({
|
||||
children,
|
||||
type = "button",
|
||||
className,
|
||||
loading = false,
|
||||
...props
|
||||
}: ButtonProps): JSX.Element {
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
className={clsx(styles.button, loading && styles.loading, className)}
|
||||
// eslint-disable-next-line react/button-has-type
|
||||
type={type}
|
||||
aria-busy={loading}
|
||||
>
|
||||
<div className={styles.spinnerWrapper} aria-hidden>
|
||||
<div className={styles.spinner} />
|
||||
</div>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default Button;
|
||||
3
server/ui/src/components/Button/index.ts
Normal file
3
server/ui/src/components/Button/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import Button from "./Button";
|
||||
|
||||
export default Button;
|
||||
89
server/ui/src/components/Codeblock/Codeblock.module.css
Normal file
89
server/ui/src/components/Codeblock/Codeblock.module.css
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
.codeblock {
|
||||
box-shadow: inset var(--ifm-global-shadow-lw);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:where(.buttonWrapper) {
|
||||
--codeblock-button-text-color: var(--text-main);
|
||||
--codeblock-button-background-color: var(--surface-2);
|
||||
--codeblock-button-background-color-hover: var(--surface-accent-1);
|
||||
--codeblock-button-separator-color: var(--surface-4);
|
||||
--codeblock-button-border-color: var(--codeblock-button-separator-color);
|
||||
--codeblock-button-icon-size: 1.5rem;
|
||||
--codeblock-button-size: 2rem;
|
||||
--codeblock-button-shadow: var(--ifm-global-shadow-tl);
|
||||
}
|
||||
|
||||
:where([data-theme="dark"] .buttonWrapper) {
|
||||
--codeblock-button-text-color: var(--text-0);
|
||||
--codeblock-button-background-color: var(--surface-6);
|
||||
--codeblock-button-background-color-hover: var(--surface-5);
|
||||
--codeblock-button-separator-color: var(--codeblock-button-text-color);
|
||||
--codeblock-button-shadow: var(--ifm-global-shadow-tl);
|
||||
--codeblock-button-shadow: none;
|
||||
}
|
||||
|
||||
.button {
|
||||
position: relative;
|
||||
width: var(--codeblock-button-size);
|
||||
height: var(--codeblock-button-size);
|
||||
background: var(--codeblock-button-background-color);
|
||||
font-family: inherit;
|
||||
font-size: 1rem;
|
||||
font-size: 0.875rem;
|
||||
text-transform: uppercase;
|
||||
color: var(--codeblock-button-text-color);
|
||||
font-weight: var(--ifm-font-weight-semibold);
|
||||
border: none;
|
||||
padding: 0;
|
||||
height: 2.25em;
|
||||
line-height: 1;
|
||||
border-radius: var(--border-radius-small);
|
||||
border-radius: 0;
|
||||
cursor: pointer;
|
||||
transition: color 200ms ease, background-color 200ms ease;
|
||||
}
|
||||
|
||||
.button:not(:first-child) {
|
||||
border-left: 1px solid var(--codeblock-button-separator-color);
|
||||
}
|
||||
.button:first-child {
|
||||
border-top-left-radius: var(--border-radius-small);
|
||||
border-bottom-left-radius: var(--border-radius-small);
|
||||
}
|
||||
.button:last-child {
|
||||
border-top-right-radius: var(--border-radius-small);
|
||||
border-bottom-right-radius: var(--border-radius-small);
|
||||
}
|
||||
|
||||
.button:hover,
|
||||
.button:focus {
|
||||
background: var(--codeblock-button-background-color-hover);
|
||||
}
|
||||
|
||||
.button svg {
|
||||
width: var(--codeblock-button-icon-size);
|
||||
height: var(--codeblock-button-icon-size);
|
||||
}
|
||||
|
||||
.buttonWrapper {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
z-index: 1;
|
||||
box-shadow: var(--codeblock-button-shadow);
|
||||
border: 1px solid var(--codeblock-button-border-color);
|
||||
border-radius: var(--border-radius-small);
|
||||
opacity: 0.75;
|
||||
transition: opacity 300ms ease;
|
||||
}
|
||||
|
||||
.buttonWrapper:hover,
|
||||
.buttonWrapper:focus-within {
|
||||
opacity: 1;
|
||||
}
|
||||
119
server/ui/src/components/Codeblock/Codeblock.tsx
Normal file
119
server/ui/src/components/Codeblock/Codeblock.tsx
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import type { PrismTheme, Language } from "prism-react-renderer";
|
||||
import Highlight, { defaultProps } from "prism-react-renderer";
|
||||
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
|
||||
import clsx from "clsx";
|
||||
import downloadFile from "js-file-download";
|
||||
import styles from "./Codeblock.module.css";
|
||||
|
||||
type ThemeValue = "light" | "dark";
|
||||
|
||||
const getTheme = () =>
|
||||
(document.documentElement.dataset.theme || "light") as ThemeValue;
|
||||
|
||||
function useGlobalTheme() {
|
||||
const [theme, setTheme] = useState<ThemeValue>(getTheme);
|
||||
useEffect(() => {
|
||||
const mo = new MutationObserver(() => {
|
||||
setTheme(getTheme());
|
||||
});
|
||||
mo.observe(document.documentElement, {
|
||||
subtree: false,
|
||||
attributeFilter: ["data-theme"],
|
||||
});
|
||||
return () => mo.disconnect();
|
||||
});
|
||||
return theme;
|
||||
}
|
||||
|
||||
function Codeblock({
|
||||
children,
|
||||
language = "markup",
|
||||
enableCopy = false,
|
||||
download,
|
||||
}: {
|
||||
children: string;
|
||||
language?: Language;
|
||||
enableCopy?: boolean;
|
||||
download?: { fileName: string; mime: string };
|
||||
}): JSX.Element {
|
||||
const { siteConfig } = useDocusaurusContext();
|
||||
const theme = useGlobalTheme();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const codeThemeLight = (siteConfig.themeConfig.prism as any)
|
||||
.theme as PrismTheme;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const codeThemeDark = (siteConfig.themeConfig.prism as any)
|
||||
.darkTheme as PrismTheme;
|
||||
const codeTheme = theme === "light" ? codeThemeLight : codeThemeDark;
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
navigator.clipboard.writeText(children);
|
||||
} catch {
|
||||
// Copying did unfortunately not work, but we'll not crash the app
|
||||
// beacause of that...
|
||||
}
|
||||
};
|
||||
const handleDownload = () => {
|
||||
if (!download || !download.fileName || !download.mime) return;
|
||||
downloadFile(children, download.fileName, download.mime);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<Highlight
|
||||
{...defaultProps}
|
||||
code={children}
|
||||
language={language}
|
||||
theme={codeTheme}
|
||||
>
|
||||
{({ className, style, tokens, getLineProps, getTokenProps }) => (
|
||||
<pre className={clsx(className, styles.codeblock)} style={style}>
|
||||
{tokens.map((line, i) => (
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<div {...getLineProps({ line, key: i })}>
|
||||
{line.map((token, key) => (
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<span {...getTokenProps({ token, key })} />
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</pre>
|
||||
)}
|
||||
</Highlight>
|
||||
{(enableCopy || download) && (
|
||||
<div className={styles.buttonWrapper}>
|
||||
{enableCopy && (
|
||||
<button
|
||||
className={styles.button}
|
||||
type="button"
|
||||
aria-label="Copy content"
|
||||
title="Copy content"
|
||||
onClick={handleCopy}
|
||||
>
|
||||
<svg aria-hidden="true" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
{download && (
|
||||
<button
|
||||
className={styles.button}
|
||||
type="button"
|
||||
aria-label="Download content as file"
|
||||
title="Download content as file"
|
||||
onClick={handleDownload}
|
||||
>
|
||||
<svg aria-hidden="true" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M5 20h14v-2H5v2zM19 9h-4V3H9v6H5l7 7 7-7z" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Codeblock;
|
||||
3
server/ui/src/components/Codeblock/index.ts
Normal file
3
server/ui/src/components/Codeblock/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import Codeblock from "./Codeblock";
|
||||
|
||||
export default Codeblock;
|
||||
94
server/ui/src/components/Dropzone/Dropzone.module.css
Normal file
94
server/ui/src/components/Dropzone/Dropzone.module.css
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
:where(.dropzone) {
|
||||
--dropzone-color-text: var(--text-faded);
|
||||
--dropzone-icon-active: var(--text-accent-2);
|
||||
--dropzone-color-background: var(--surface-2);
|
||||
--dropzone-color-background-active: var(--surface-accent-0);
|
||||
--dropzone-color-border: var(--color-border);
|
||||
--dropzone-color-border-active: var(--color-border-accent);
|
||||
--dropzone-opacity-hover-preview: 0;
|
||||
--dropzone-shadow-opacity: 0;
|
||||
--dropzone-border-size: 0.2rem;
|
||||
}
|
||||
|
||||
:where([data-theme="dark"] .dropzone) {
|
||||
--dropzone-color-text: var(--text-0);
|
||||
--dropzone-icon-active: var(--text-accent-0);
|
||||
--dropzone-color-background: transparent;
|
||||
--dropzone-color-background-active: var(--surface-accent-5);
|
||||
}
|
||||
|
||||
.dropzone {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
height: 15em;
|
||||
color: var(--dropzone-color-text);
|
||||
background: var(--dropzone-color-background);
|
||||
border: var(--dropzone-border-size) dashed var(--dropzone-color-border);
|
||||
border-radius: var(--border-radius-medium);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: var(--ifm-global-shadow-lw);
|
||||
}
|
||||
|
||||
.dropzone::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
box-shadow: var(--ifm-global-shadow-md);
|
||||
z-index: -1;
|
||||
opacity: var(--dropzone-shadow-opacity);
|
||||
transition: opacity 200ms ease;
|
||||
}
|
||||
|
||||
.active {
|
||||
--dropzone-opacity-hover-preview: 0.8;
|
||||
--dropzone-shadow-opacity: 1;
|
||||
}
|
||||
|
||||
.hasFiles {
|
||||
--dropzone-color-background: var(--dropzone-color-background-active);
|
||||
--dropzone-color-border: var(--dropzone-color-border-active);
|
||||
--dropzone-shadow-opacity: 0.25;
|
||||
}
|
||||
|
||||
.fileHoverPreview {
|
||||
--dropzone-color-background: var(--dropzone-color-background-active);
|
||||
--dropzone-color-border: var(--dropzone-color-border-active);
|
||||
|
||||
position: absolute;
|
||||
top: calc(-1 * var(--dropzone-border-size));
|
||||
right: calc(-1 * var(--dropzone-border-size));
|
||||
bottom: calc(-1 * var(--dropzone-border-size));
|
||||
left: calc(-1 * var(--dropzone-border-size));
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--dropzone-color-text);
|
||||
background: var(--dropzone-color-background);
|
||||
border: var(--dropzone-border-size) dashed var(--dropzone-color-border);
|
||||
border-radius: var(--border-radius-medium);
|
||||
opacity: var(--dropzone-opacity-hover-preview);
|
||||
transition: opacity 150ms ease-in-out;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 3rem;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
.fileHoverIcon {
|
||||
font-size: 5rem;
|
||||
color: var(--dropzone-icon-active);
|
||||
}
|
||||
|
||||
.uploadIcon {
|
||||
color: var(--dropzone-color-text);
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
80
server/ui/src/components/Dropzone/Dropzone.tsx
Normal file
80
server/ui/src/components/Dropzone/Dropzone.tsx
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
import React from "react";
|
||||
import clsx from "clsx";
|
||||
import type { DropEvent } from "react-dropzone";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import type { DropzoneProps, RejectionType } from "./types";
|
||||
import styles from "./Dropzone.module.css";
|
||||
|
||||
const Dropzone = ({
|
||||
accept,
|
||||
children,
|
||||
className,
|
||||
activeClassName,
|
||||
multiple = false,
|
||||
name,
|
||||
onDrop,
|
||||
hasSelectedFiles,
|
||||
...props
|
||||
}: DropzoneProps): JSX.Element => {
|
||||
const handleDrop = (
|
||||
accepted: File[],
|
||||
fileRejections: RejectionType[],
|
||||
event: DropEvent,
|
||||
) => {
|
||||
const rejected = fileRejections.map((rejection) => rejection.file);
|
||||
onDrop(accepted, rejected, event);
|
||||
};
|
||||
const {
|
||||
getRootProps,
|
||||
getInputProps,
|
||||
isDragActive,
|
||||
isDragAccept,
|
||||
isDragReject,
|
||||
} = useDropzone({ accept, multiple, onDrop: handleDrop, ...props });
|
||||
return (
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className={clsx(
|
||||
styles.dropzone,
|
||||
isDragActive && styles.active,
|
||||
hasSelectedFiles && styles.hasFiles,
|
||||
className,
|
||||
isDragActive && activeClassName,
|
||||
)}
|
||||
data-testid="dropzone"
|
||||
data-is-drag-active={isDragActive}
|
||||
data-is-drag-accepted={isDragAccept}
|
||||
data-is-drag-rejected={isDragReject}
|
||||
data-has-files={hasSelectedFiles}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
styles.fileHoverPreview,
|
||||
isDragActive && styles.previewActive,
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
className={clsx(styles.icon, styles.fileHoverIcon)}
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M6 2c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6H6zm7 7V3.5L18.5 9H13z" />
|
||||
</svg>
|
||||
</div>
|
||||
<svg
|
||||
className={clsx(styles.icon, styles.uploadIcon)}
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M9 16h6v-6h4l-7-7-7 7h4zm-4 2h14v2H5z" />
|
||||
</svg>
|
||||
{children}
|
||||
<input name={name} {...getInputProps()} data-testid="dropzone-input" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dropzone;
|
||||
5
server/ui/src/components/Dropzone/index.ts
Normal file
5
server/ui/src/components/Dropzone/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import Dropzone from "./Dropzone";
|
||||
|
||||
export { default as useDropzone } from "./useDropzone";
|
||||
|
||||
export default Dropzone;
|
||||
24
server/ui/src/components/Dropzone/types.ts
Normal file
24
server/ui/src/components/Dropzone/types.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import type { ReactNode, RefAttributes } from "react";
|
||||
import type {
|
||||
DropEvent,
|
||||
DropzoneProps as ReactDropzoneProps,
|
||||
DropzoneRef,
|
||||
} from "react-dropzone";
|
||||
import type { ExtendProps } from "../util/types";
|
||||
|
||||
export interface RejectionType {
|
||||
file: File;
|
||||
}
|
||||
|
||||
export type DropzoneProps = ExtendProps<
|
||||
ReactDropzoneProps & RefAttributes<DropzoneRef>,
|
||||
{
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
activeClassName?: string;
|
||||
multiple?: boolean;
|
||||
hasSelectedFiles?: boolean;
|
||||
name?: string;
|
||||
onDrop: (accepted: File[], rejections: File[], event: DropEvent) => void;
|
||||
}
|
||||
>;
|
||||
53
server/ui/src/components/Dropzone/useDropzone.ts
Normal file
53
server/ui/src/components/Dropzone/useDropzone.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { useCallback, useMemo, useState } from "react";
|
||||
|
||||
interface DropzoneHelpers {
|
||||
selectedFiles: File[];
|
||||
rejectedFiles: File[];
|
||||
hasSelectedFiles: boolean;
|
||||
getProps: () => {
|
||||
onDrop: (accepted: File[], rejected: File[]) => void;
|
||||
multiple: boolean;
|
||||
accept: string | string[];
|
||||
hasSelectedFiles: boolean;
|
||||
};
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
function useDropzone({
|
||||
multiple = false,
|
||||
accept,
|
||||
}: {
|
||||
multiple?: boolean;
|
||||
accept: string | string[];
|
||||
}): DropzoneHelpers {
|
||||
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
|
||||
const [rejectedFiles, setRejectedFiles] = useState<File[]>([]);
|
||||
|
||||
const hasSelectedFiles = selectedFiles.length > 0;
|
||||
|
||||
const getProps = useMemo(() => {
|
||||
const handleDrop = (accepted: File[], rejected: File[]) => {
|
||||
setSelectedFiles(accepted);
|
||||
if (rejected.length === 0) {
|
||||
setRejectedFiles([]);
|
||||
} else {
|
||||
setRejectedFiles(rejected);
|
||||
}
|
||||
};
|
||||
return () => ({
|
||||
onDrop: handleDrop,
|
||||
multiple,
|
||||
accept,
|
||||
hasSelectedFiles,
|
||||
});
|
||||
}, [accept, hasSelectedFiles, multiple]);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setSelectedFiles([]);
|
||||
setRejectedFiles([]);
|
||||
}, []);
|
||||
|
||||
return { selectedFiles, rejectedFiles, hasSelectedFiles, getProps, reset };
|
||||
}
|
||||
|
||||
export default useDropzone;
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
.errorDisplay {
|
||||
background-color: var(--red-3);
|
||||
color: var(--text-accent-bg-0);
|
||||
padding: 0.75em 1.25em;
|
||||
border-radius: var(--ifm-global-radius);
|
||||
box-shadow: var(--ifm-global-shadow-lw);
|
||||
margin: 1em 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75em;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: block;
|
||||
line-height: 1;
|
||||
}
|
||||
20
server/ui/src/components/ErrorDisplay/ErrorDisplay.tsx
Normal file
20
server/ui/src/components/ErrorDisplay/ErrorDisplay.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import type { ReactNode } from "react";
|
||||
import React from "react";
|
||||
import styles from "./ErrorDisplay.module.css";
|
||||
|
||||
function ErrorDisplay({
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
children?: ReactNode;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div role="alert" className={styles.errorDisplay}>
|
||||
<strong className={styles.title}>{title}</strong>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ErrorDisplay;
|
||||
3
server/ui/src/components/ErrorDisplay/index.ts
Normal file
3
server/ui/src/components/ErrorDisplay/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import ErrorDisplay from "./ErrorDisplay";
|
||||
|
||||
export default ErrorDisplay;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
.headline {
|
||||
font-size: 3rem;
|
||||
}
|
||||
30
server/ui/src/components/PageLayout/PageLayout.tsx
Normal file
30
server/ui/src/components/PageLayout/PageLayout.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import type { ReactNode } from "react";
|
||||
import React from "react";
|
||||
import Layout from "@theme/Layout";
|
||||
import styles from "./PageLayout.module.css";
|
||||
|
||||
function PageLayout({
|
||||
children,
|
||||
layoutDescription,
|
||||
description,
|
||||
title,
|
||||
headline,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
layoutDescription: string;
|
||||
description: string;
|
||||
headline: string;
|
||||
title?: string;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<Layout description={layoutDescription} title={title}>
|
||||
<main className="container padding-top--md padding-bottom--lg">
|
||||
<h1 className={styles.headline}>{headline}</h1>
|
||||
<p>{description}</p>
|
||||
{children}
|
||||
</main>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export default PageLayout;
|
||||
3
server/ui/src/components/PageLayout/index.ts
Normal file
3
server/ui/src/components/PageLayout/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import PageLayout from "./PageLayout";
|
||||
|
||||
export default PageLayout;
|
||||
14
server/ui/src/components/Upload/Upload.module.css
Normal file
14
server/ui/src/components/Upload/Upload.module.css
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
.buttonGroup {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.resultDisplay {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.withError {
|
||||
border: 0.2rem solid var(--text-error);
|
||||
border-radius: var(--border-radius-small);
|
||||
}
|
||||
115
server/ui/src/components/Upload/Upload.tsx
Normal file
115
server/ui/src/components/Upload/Upload.tsx
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
import type { FormEventHandler } from "react";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import clsx from "clsx";
|
||||
import Dropzone from "../Dropzone";
|
||||
import Codeblock from "../Codeblock";
|
||||
import ErrorDisplay from "../ErrorDisplay";
|
||||
import useRequest, { RequestStatus } from "../util/useRequest";
|
||||
import Button from "../Button";
|
||||
import styles from "./Upload.module.css";
|
||||
|
||||
const ENDPOINT = "/";
|
||||
|
||||
const ACCEPT = {
|
||||
"text/xml": [".xml", ".XML"],
|
||||
"application/xml": [".xml", ".XML"],
|
||||
};
|
||||
|
||||
function createFileName(selectedFileName: string | undefined) {
|
||||
return selectedFileName
|
||||
? `${selectedFileName.replace(/\.xml$/i, "")}-report.xml`
|
||||
: "report.xml";
|
||||
}
|
||||
|
||||
function Upload(): JSX.Element {
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
const [rejected, setRejected] = useState<File[]>([]);
|
||||
const { data, error, request, status } = useRequest();
|
||||
|
||||
const handleDrop = useCallback(
|
||||
(acceptedFiles: File[], rejectedFiles: File[]) => {
|
||||
if (acceptedFiles.length) {
|
||||
setSelectedFile(acceptedFiles[0]);
|
||||
setRejected([]);
|
||||
} else {
|
||||
setRejected(rejectedFiles);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleSubmit: FormEventHandler<HTMLFormElement> = (e) => {
|
||||
e.preventDefault();
|
||||
if (!selectedFile) return;
|
||||
request(ENDPOINT, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/xml" },
|
||||
body: selectedFile,
|
||||
redirect: "follow",
|
||||
});
|
||||
};
|
||||
|
||||
const meaningfulErrorResponse = !!error && [406, 422].includes(error.code);
|
||||
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={handleSubmit}>
|
||||
{status === RequestStatus.Failure && error && !meaningfulErrorResponse && (
|
||||
<ErrorDisplay title="An error occurred while validating the file">
|
||||
<Codeblock enableCopy>{error.message}</Codeblock>
|
||||
</ErrorDisplay>
|
||||
)}
|
||||
{rejected.length > 1 && (
|
||||
<ErrorDisplay title="Please select a single file only" />
|
||||
)}
|
||||
{rejected.length === 1 && (
|
||||
<ErrorDisplay title="Only XML files are supported">
|
||||
<Codeblock>{`Invalid file found: ${rejected[0].name}`}</Codeblock>
|
||||
</ErrorDisplay>
|
||||
)}
|
||||
<Dropzone
|
||||
onDrop={handleDrop}
|
||||
accept={ACCEPT}
|
||||
multiple={false}
|
||||
hasSelectedFiles={!!selectedFile}
|
||||
>
|
||||
{selectedFile ? (
|
||||
selectedFile.name
|
||||
) : (
|
||||
<>Drag & drop files here or click to select a file</>
|
||||
)}
|
||||
</Dropzone>
|
||||
<div className={styles.buttonGroup}>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!selectedFile}
|
||||
loading={status === RequestStatus.Loading}
|
||||
>
|
||||
Validate
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
{((data && status === RequestStatus.Success) ||
|
||||
meaningfulErrorResponse) && (
|
||||
<div
|
||||
className={clsx(
|
||||
styles.resultDisplay,
|
||||
meaningfulErrorResponse && styles.withError,
|
||||
)}
|
||||
>
|
||||
<Codeblock
|
||||
download={{
|
||||
fileName: createFileName(selectedFile?.name),
|
||||
mime: "application/xml",
|
||||
}}
|
||||
enableCopy
|
||||
>
|
||||
{data || error?.message || ""}
|
||||
</Codeblock>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Upload;
|
||||
3
server/ui/src/components/Upload/index.ts
Normal file
3
server/ui/src/components/Upload/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import Upload from "./Upload";
|
||||
|
||||
export default Upload;
|
||||
35
server/ui/src/components/XmlView/XmlView.tsx
Normal file
35
server/ui/src/components/XmlView/XmlView.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import React, { useEffect } from "react";
|
||||
import Codeblock from "../Codeblock";
|
||||
import ErrorDisplay from "../ErrorDisplay";
|
||||
import useRequest, { RequestStatus } from "../util/useRequest";
|
||||
|
||||
function XmlView({
|
||||
endpoint,
|
||||
fileName,
|
||||
}: {
|
||||
endpoint: string;
|
||||
fileName: string;
|
||||
}): JSX.Element {
|
||||
const { request, data, error, status } = useRequest();
|
||||
|
||||
useEffect(() => {
|
||||
request(endpoint, { headers: { "Content-Type": "application/xml" } });
|
||||
}, [endpoint, request]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{status === RequestStatus.Failure && error && (
|
||||
<ErrorDisplay title="An error occurred while fetching">
|
||||
<Codeblock>{error.message}</Codeblock>
|
||||
</ErrorDisplay>
|
||||
)}
|
||||
{status === RequestStatus.Success && data && (
|
||||
<Codeblock download={{ fileName, mime: "application/xml" }} enableCopy>
|
||||
{data}
|
||||
</Codeblock>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default XmlView;
|
||||
3
server/ui/src/components/XmlView/index.ts
Normal file
3
server/ui/src/components/XmlView/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import XmlView from "./XmlView";
|
||||
|
||||
export default XmlView;
|
||||
6
server/ui/src/components/util/types.ts
Normal file
6
server/ui/src/components/util/types.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export type SharedKeys<A, B> = Extract<keyof A, keyof B>;
|
||||
export type ExtendProps<Base, Extension> = Omit<
|
||||
Base,
|
||||
SharedKeys<Base, Extension>
|
||||
> &
|
||||
Extension;
|
||||
93
server/ui/src/components/util/useRequest.ts
Normal file
93
server/ui/src/components/util/useRequest.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
|
||||
|
||||
export enum RequestStatus {
|
||||
Idle = "idle",
|
||||
Loading = "loading",
|
||||
Success = "success",
|
||||
Failure = "failure",
|
||||
}
|
||||
|
||||
export interface RequestState {
|
||||
status: RequestStatus;
|
||||
data: null | string;
|
||||
error: null | { code: number; message: string };
|
||||
}
|
||||
|
||||
export interface UseRequest extends RequestState {
|
||||
request: (endpoint: string, init?: RequestInit) => void;
|
||||
}
|
||||
|
||||
const EMPTY_REQUEST: RequestState = {
|
||||
status: RequestStatus.Idle,
|
||||
data: null,
|
||||
error: null,
|
||||
};
|
||||
|
||||
function createEndpoint(endpoint: string, apiBase: string): string {
|
||||
const segments = apiBase
|
||||
.split("/")
|
||||
.concat(endpoint.split("/"))
|
||||
.filter(Boolean);
|
||||
return `/${segments.join("/")}`;
|
||||
}
|
||||
|
||||
function useRequest(): UseRequest {
|
||||
const [requestState, setRequest] = useState(EMPTY_REQUEST);
|
||||
const { siteConfig } = useDocusaurusContext();
|
||||
const apiBase = siteConfig.customFields?.apiBase as string;
|
||||
|
||||
const isMountedRef = useRef(true);
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
isMountedRef.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const request = useCallback(
|
||||
(endpoint: string, init?: RequestInit) => {
|
||||
setRequest((prev) => ({ ...prev, status: RequestStatus.Loading }));
|
||||
|
||||
fetch(createEndpoint(endpoint, apiBase), init)
|
||||
.then((response) => {
|
||||
return response.text().then((text) => ({
|
||||
data: text,
|
||||
ok: response.ok,
|
||||
code: response.status,
|
||||
}));
|
||||
})
|
||||
.then(({ data, ok, code }) => {
|
||||
if (!isMountedRef.current) return;
|
||||
if (ok) {
|
||||
setRequest({
|
||||
status: RequestStatus.Success,
|
||||
data,
|
||||
error: null,
|
||||
});
|
||||
} else {
|
||||
setRequest((prev) => ({
|
||||
...prev,
|
||||
status: RequestStatus.Failure,
|
||||
error: { code, message: data },
|
||||
}));
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
if (!isMountedRef.current) return;
|
||||
setRequest((prev) => ({
|
||||
...prev,
|
||||
status: RequestStatus.Failure,
|
||||
error: {
|
||||
code: 0,
|
||||
message: error?.toString?.() || "An unknown error occurred",
|
||||
},
|
||||
}));
|
||||
});
|
||||
},
|
||||
[apiBase],
|
||||
);
|
||||
|
||||
return useMemo(() => ({ ...requestState, request }), [request, requestState]);
|
||||
}
|
||||
|
||||
export default useRequest;
|
||||
172
server/ui/src/css/custom.css
Normal file
172
server/ui/src/css/custom.css
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
/**
|
||||
* Any CSS included here will be global. The classic template
|
||||
* bundles Infima by default. Infima is a CSS framework designed to
|
||||
* work well for content-centric websites.
|
||||
*/
|
||||
|
||||
/* You can override the default Infima variables here. */
|
||||
:root {
|
||||
--ifm-color-primary: var(--blue-3);
|
||||
--ifm-color-primary-dark: var(--blue-4);
|
||||
--ifm-color-primary-darker: var(--blue-5);
|
||||
--ifm-color-primary-darkest: var(--blue-6);
|
||||
--ifm-color-primary-light: var(--blue-2);
|
||||
--ifm-color-primary-lighter: var(--blue-1);
|
||||
--ifm-color-primary-lightest: var(--blue-0);
|
||||
|
||||
--ifm-code-font-size: 95%;
|
||||
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
|
||||
--ifm-global-radius: var(--border-radius-small);
|
||||
|
||||
--ifm-font-family-base: var(--font-sans);
|
||||
--ifm-font-family-monospace: var(--font-mono);
|
||||
}
|
||||
|
||||
/* For readability concerns, you should choose a lighter palette in dark mode. */
|
||||
[data-theme="dark"]:root {
|
||||
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
|
||||
--ifm-background-color: var(--surface-6);
|
||||
}
|
||||
|
||||
[data-theme] .footer {
|
||||
--ifm-footer-background-color: var(--surface-accent-4);
|
||||
--ifm-footer-color: var(--text-accent-bg-main);
|
||||
--ifm-footer-link-color: var(--text-accent-bg-main);
|
||||
--ifm-footer-link-hover-color: var(--text-accent-bg-main);
|
||||
--ifm-footer-title-color: var(--text-accent-bg-main);
|
||||
}
|
||||
[data-theme="dark"] .footer {
|
||||
--ifm-footer-background-color: var(--surface-6);
|
||||
border-top: 1px solid var(--ifm-table-border-color);
|
||||
}
|
||||
[data-theme] .navbar {
|
||||
--ifm-navbar-background-color: var(--blue-4);
|
||||
}
|
||||
|
||||
/* XÖV Theme */
|
||||
:where(html) {
|
||||
--blue-0: hsl(206 100% 95%);
|
||||
--blue-1: hsl(206 100% 80%);
|
||||
--blue-2: hsl(206 100% 65%);
|
||||
--blue-3: hsl(206 100% 47%);
|
||||
--blue-4: hsl(220 41% 30%);
|
||||
--blue-5: hsl(220 41% 24%);
|
||||
--blue-6: hsl(220 41% 18%);
|
||||
|
||||
--gray-0: hsl(216 33% 100%);
|
||||
--gray-1: hsl(216 33% 97%);
|
||||
--gray-2: hsl(220 21% 95%);
|
||||
--gray-3: hsl(220 21% 92%);
|
||||
--gray-4: hsl(212 10% 73%);
|
||||
--gray-5: hsl(212 10% 45%);
|
||||
--gray-6: hsl(212 15% 13%);
|
||||
|
||||
--red-0: hsl(357 80% 96%);
|
||||
--red-1: hsl(357 80% 89%);
|
||||
--red-2: hsl(357 80% 75%);
|
||||
--red-3: hsl(357 80% 60%);
|
||||
--red-4: hsl(357 80% 40%);
|
||||
--red-5: hsl(357 60% 22%);
|
||||
|
||||
--orange-0: hsl(46 80% 90%);
|
||||
--orange-1: hsl(46 80% 80%);
|
||||
--orange-2: hsl(46 80% 68%);
|
||||
--orange-3: hsl(46 80% 40%);
|
||||
--orange-4: hsl(46 80% 25%);
|
||||
--orange-5: hsl(46 80% 10%);
|
||||
|
||||
--green-0: hsl(162 100% 93%);
|
||||
--green-1: hsl(162 100% 74%);
|
||||
--green-2: hsl(162 100% 45%);
|
||||
--green-3: hsl(162 100% 30%);
|
||||
--green-4: hsl(162 100% 20%);
|
||||
--green-5: hsl(162 100% 10%);
|
||||
|
||||
/* Surface colors */
|
||||
--surface-0: var(--gray-0);
|
||||
--surface-1: var(--gray-1);
|
||||
--surface-2: var(--gray-2);
|
||||
--surface-3: var(--gray-3);
|
||||
--surface-4: var(--gray-4);
|
||||
--surface-5: var(--gray-5);
|
||||
--surface-6: var(--gray-6);
|
||||
|
||||
--surface-accent-0: var(--blue-0);
|
||||
--surface-accent-1: var(--blue-1);
|
||||
--surface-accent-2: var(--blue-2);
|
||||
--surface-accent-3: var(--blue-3);
|
||||
--surface-accent-4: var(--blue-4);
|
||||
--surface-accent-5: var(--blue-5);
|
||||
|
||||
/* Text colors */
|
||||
--text-0: var(--gray-4);
|
||||
--text-1: var(--gray-5);
|
||||
--text-2: var(--gray-6);
|
||||
--text-main: var(--text-2);
|
||||
--text-faded: var(--text-1);
|
||||
|
||||
--text-accent-0: var(--blue-3);
|
||||
--text-accent-1: var(--blue-4);
|
||||
--text-accent-2: var(--blue-5);
|
||||
--text-accent: var(--text-accent-2);
|
||||
|
||||
--text-accent-bg-0: var(--gray-0);
|
||||
--text-accent-bg-1: var(--gray-1);
|
||||
--text-accent-bg-2: var(--gray-4);
|
||||
--text-accent-bg-3: var(--blue-4);
|
||||
--text-accent-bg-main: var(--text-accent-bg-0);
|
||||
|
||||
--text-negative: var(--red-3);
|
||||
--text-error: var(--red-3);
|
||||
--text-warning: var(--orange-3);
|
||||
--text-info: var(--blue-3);
|
||||
--text-success: var(--green-3);
|
||||
|
||||
/* Misc elements */
|
||||
--divider: var(--gray-4);
|
||||
--scrollthumb: var(--gray-4);
|
||||
--input-background: var(--surface-0);
|
||||
--input-background-disabled: var(--surface-2);
|
||||
|
||||
/* Border Radius */
|
||||
--border-radius-small: 2px;
|
||||
--border-radius-medium: 0.2rem;
|
||||
--border-radius-large: 1rem;
|
||||
|
||||
--color-border: var(--gray-4);
|
||||
--color-border-hover: var(--gray-4);
|
||||
--color-border-accent: var(--blue-2);
|
||||
--color-border-accent-hover: var(--blue-2);
|
||||
|
||||
/* Fonts */
|
||||
--font-sans: "Open Sans", system-ui, -apple-system, "Segoe UI", "Roboto",
|
||||
"Ubuntu", "Cantarell", "Noto Sans", sans-serif, "Apple Color Emoji",
|
||||
"Segoe UI Emoji", "Segoe UI Symbol";
|
||||
--font-serif: ui-serif, serif, "Apple Color Emoji", "Segoe UI Emoji",
|
||||
"Segoe UI Symbol";
|
||||
--font-mono: "Cascadia Code", "Dank Mono", "Operator Mono", "Inconsolata",
|
||||
"Fira Mono", ui-monospace, "SF Mono", "Monaco", "Droid Sans Mono",
|
||||
"Source Code Pro", monospace, "Apple Color Emoji", "Segoe UI Emoji",
|
||||
"Segoe UI Symbol";
|
||||
}
|
||||
|
||||
/* Scrollbars: */
|
||||
/* Firefox */
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--scrollthumb) transparent;
|
||||
}
|
||||
/* Chrome, Edge, and Safari */
|
||||
*::-webkit-scrollbar {
|
||||
width: min(8px, 0.5rem);
|
||||
height: min(8px, 0.5rem);
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: var(--scrollthumb);
|
||||
border-radius: 999rem;
|
||||
}
|
||||
18
server/ui/src/pages/config/ConfigPage.tsx
Normal file
18
server/ui/src/pages/config/ConfigPage.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import React from "react";
|
||||
import PageLayout from "@site/src/components/PageLayout";
|
||||
import XmlView from "@site/src/components/XmlView";
|
||||
|
||||
function ConfigPage(): JSX.Element {
|
||||
return (
|
||||
<PageLayout
|
||||
title="Validator configuration"
|
||||
layoutDescription="The currently loaded validator configuration"
|
||||
headline="Validator configuration"
|
||||
description="View the currently loaded validator configuration."
|
||||
>
|
||||
<XmlView endpoint="/server/config" fileName="config.xml" />
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default ConfigPage;
|
||||
3
server/ui/src/pages/config/index.ts
Normal file
3
server/ui/src/pages/config/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import ConfigPage from "./ConfigPage";
|
||||
|
||||
export default ConfigPage;
|
||||
18
server/ui/src/pages/health/HealthPage.tsx
Normal file
18
server/ui/src/pages/health/HealthPage.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import React from "react";
|
||||
import PageLayout from "@site/src/components/PageLayout";
|
||||
import XmlView from "@site/src/components/XmlView";
|
||||
|
||||
function HealthPage(): JSX.Element {
|
||||
return (
|
||||
<PageLayout
|
||||
title="Health information"
|
||||
layoutDescription="Health and status information about the system"
|
||||
headline="Server health information"
|
||||
description="Information about health and status of the running system."
|
||||
>
|
||||
<XmlView endpoint="/server/health" fileName="health.xml" />
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default HealthPage;
|
||||
3
server/ui/src/pages/health/index.ts
Normal file
3
server/ui/src/pages/health/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import HealthPage from "./HealthPage";
|
||||
|
||||
export default HealthPage;
|
||||
15
server/ui/src/pages/index.tsx
Normal file
15
server/ui/src/pages/index.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import React from "react";
|
||||
import Upload from "../components/Upload";
|
||||
import PageLayout from "../components/PageLayout";
|
||||
|
||||
export default function Home(): JSX.Element {
|
||||
return (
|
||||
<PageLayout
|
||||
layoutDescription="KoSIT Validator Daemon"
|
||||
headline="Try the validator!"
|
||||
description="Upload an XML file here to validate its contents. Note: this is just a demo implementation, not meant for production usage. If you need a production ready implementation you are welcome to contribute to the open source project."
|
||||
>
|
||||
<Upload />
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
7
server/ui/src/pages/markdown-page.md
Normal file
7
server/ui/src/pages/markdown-page.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
title: Markdown page example
|
||||
---
|
||||
|
||||
# Markdown page example
|
||||
|
||||
You don't need React to write simple standalone pages.
|
||||
Loading…
Add table
Add a link
Reference in a new issue