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
18
server/ui/.eslintignore
Normal file
18
server/ui/.eslintignore
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
**/build/
|
||||
**/dist/
|
||||
**/coverage/
|
||||
**/.nyc_output
|
||||
**/.husky
|
||||
**/.vscode
|
||||
**/.webpack
|
||||
packages/electron/out
|
||||
**/node_modules/
|
||||
**/tmp/
|
||||
**/package-lock.json
|
||||
**/pnpm-lock.yaml
|
||||
**/yarn.lock
|
||||
**/package.json
|
||||
**/tsconfig.json
|
||||
**/*.html
|
||||
packages/*/types
|
||||
.docusaurus
|
||||
55
server/ui/.eslintrc
Normal file
55
server/ui/.eslintrc
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"plugins": ["prettier", "@typescript-eslint/eslint-plugin", "react"],
|
||||
"extends": [
|
||||
"airbnb",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:prettier/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"indent": "off",
|
||||
"@typescript-eslint/indent": "off",
|
||||
"@typescript-eslint/explicit-member-accessibility": "off",
|
||||
"@typescript-eslint/consistent-type-imports": [
|
||||
"error",
|
||||
{ "prefer": "type-imports" }
|
||||
],
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react/prop-types": "off",
|
||||
"react/jsx-props-no-spreading": "off",
|
||||
"react/require-default-props": "off",
|
||||
"react/jsx-filename-extension": "off",
|
||||
"react/jsx-one-expression-per-line": "off",
|
||||
"react/function-component-definition": "off",
|
||||
"react/jsx-no-useless-fragment": "off",
|
||||
"import/extensions": "off",
|
||||
"import/no-unresolved": "off",
|
||||
"no-console": "warn",
|
||||
"no-shadow": "off",
|
||||
"no-continue": "off",
|
||||
"no-restricted-syntax": "off",
|
||||
"@typescript-eslint/no-shadow": ["error"],
|
||||
"import/no-relative-packages": "off"
|
||||
},
|
||||
"env": {
|
||||
"es6": true,
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"jest": true
|
||||
},
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "17.0"
|
||||
},
|
||||
"import/parsers": {
|
||||
"@typescript-eslint/parser": [".ts", ".tsx"]
|
||||
},
|
||||
"import/resolver": {
|
||||
"typescript": {
|
||||
"alwaysTryTypes": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
20
server/ui/.gitignore
vendored
Normal file
20
server/ui/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# Dependencies
|
||||
/node_modules
|
||||
|
||||
# Production
|
||||
/build
|
||||
|
||||
# Generated files
|
||||
.docusaurus
|
||||
.cache-loader
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
20
server/ui/.prettierignore
Normal file
20
server/ui/.prettierignore
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
**/build/
|
||||
**/dist/
|
||||
**/build/
|
||||
**/coverage/
|
||||
**/.nyc_output
|
||||
**/.husky
|
||||
**/.vscode
|
||||
**/.webpack
|
||||
packages/electron/out
|
||||
**/node_modules/
|
||||
**/tmp
|
||||
**/package-lock.json
|
||||
**/pnpm-lock.yaml
|
||||
**/yarn.lock
|
||||
**/package.json
|
||||
packages/*/types
|
||||
**/*.yaml
|
||||
**/*.yml
|
||||
docs
|
||||
.docusaurus
|
||||
6
server/ui/.prettierrc.js
Normal file
6
server/ui/.prettierrc.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
trailingComma: "all",
|
||||
useTabs: true,
|
||||
proseWrap: "always",
|
||||
endOfLine: "auto",
|
||||
};
|
||||
27
server/ui/README.md
Normal file
27
server/ui/README.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Website
|
||||
|
||||
This folder contains the ui, served by the daemon version of the validator. At
|
||||
the moment, this is generated within this module and copied to the actual source
|
||||
location of the daemon. There are plans to modularize the whole validator source
|
||||
in the future so that this will be done by build process.
|
||||
|
||||
This ui is built using [Docusaurus 2](https://docusaurus.io/), a modern static
|
||||
website generator.
|
||||
|
||||
### Local Development
|
||||
|
||||
```
|
||||
$ npm start
|
||||
```
|
||||
|
||||
This command starts a local development server and opens up a browser window.
|
||||
Most changes are reflected live without having to restart the server.
|
||||
|
||||
### Build
|
||||
|
||||
```
|
||||
$ npm build
|
||||
```
|
||||
|
||||
This command generates static content into the `build` directory and must be
|
||||
copied to `src/main/resources/ui`
|
||||
3
server/ui/babel.config.js
Normal file
3
server/ui/babel.config.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
presets: [require.resolve("@docusaurus/core/lib/babel/preset")],
|
||||
};
|
||||
51
server/ui/docs/api.md
Normal file
51
server/ui/docs/api.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
---
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
# API Usage
|
||||
|
||||
The validation service listens to `POST`-requests to any server uri. You need to supply the xml/object to validate in
|
||||
the post body.
|
||||
The service expects a single plain input in the post body, e.g. `multipart/form-data` is not supported.
|
||||
|
||||
Examples:
|
||||
|
||||
* `cURL`
|
||||
|
||||
```shell script
|
||||
curl --location --request POST 'http://localhost:8080' \
|
||||
--header 'Content-Type: application/xml' \
|
||||
--data-binary '@/target.xml'
|
||||
```
|
||||
|
||||
* `java` (Apache HttpClient)
|
||||
|
||||
```java
|
||||
HttpClient httpClient=HttpClientBuilder.create().build();
|
||||
HttpPost postRequest=new HttpPost("http://localhost:8080/");
|
||||
FileEntity entity=new FileEntity(Paths.get("some.xml").toFile(),ContentType.APPLICATION_XML);
|
||||
postRequest.setEntity(entity);
|
||||
HttpResponse response=httpClient.execute(postRequest);
|
||||
System.out.println(IOUtils.toString(response.getEntity().getContent()));
|
||||
```
|
||||
|
||||
* `javascript`
|
||||
|
||||
```javascript
|
||||
var myHeaders = new Headers();
|
||||
myHeaders.append("Content-Type", "application/xml");
|
||||
|
||||
var file = "<file contents here>";
|
||||
|
||||
var requestOptions = {
|
||||
method: 'POST',
|
||||
headers: myHeaders,
|
||||
body: file,
|
||||
redirect: 'follow'
|
||||
};
|
||||
|
||||
fetch("http://localhost:8080", requestOptions)
|
||||
.then(response => response.text())
|
||||
.then(result => console.log(result))
|
||||
.catch(error => console.log('error', error));
|
||||
```
|
||||
201
server/ui/docs/changelog.md
Normal file
201
server/ui/docs/changelog.md
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
# 1.5.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- (CLI) [#93](https://projekte.kosit.org/kosit/validator/-/issues/93) Remove usage information, when validation failed
|
||||
- (CLI) [#95](https://projekte.kosit.org/kosit/validator/-/issues/95) NPE when using empty repository definition (-r "")
|
||||
|
||||
### Added
|
||||
|
||||
- (CLI) Support for multiple configurations and multiple repositories. See [cli documentation](https://github.com/itplr-kosit/validator/blob/master/docs/cli.md) for details
|
||||
- (API) Possibility to use preconfigured Saxon `Processor` instance for validation
|
||||
|
||||
### Changed
|
||||
|
||||
- (DAEMON) UI rewrite based on [Docusaurs](https://docusaurus.io)
|
||||
- (
|
||||
API) [ResolvingConfigurationStrategy.java#getProcessor()](https://github.com/itplr-kosit/validator/blob/master/src/main/java/de/kosit/validationtool/api/ResolvingConfigurationStrategy)
|
||||
is removed.
|
||||
- (CORE) Bump [Saxon HE](https://www.saxonica.com/documentation11/documentation.xml) to 11.4
|
||||
- (CORE) Bump [jaxb-ri](https://github.com/eclipse-ee4j/jaxb-ri) to 2.3.7
|
||||
|
||||
- (CORE) CLI parsing based on pico-cli, commons-cli is removed
|
||||
|
||||
## 1.4.2
|
||||
|
||||
### Fixed
|
||||
|
||||
- (CLI) [#74](https://projekte.kosit.org/kosit/validator/-/issues/74) fix ansi output of the cli version
|
||||
- [#80](https://github.com/itplr-kosit/validator/issues/80) using classloader to initialize jaxb context (to support
|
||||
usage in OSGi
|
||||
environments)
|
||||
- [#75] (https://github.com/itplr-kosit/validator/issues/75) Improve logging on invalid documents
|
||||
|
||||
## 1.4.1
|
||||
|
||||
### Fixed
|
||||
|
||||
- Allow more than 3 customLevel elements in scenarios (see xrechnung
|
||||
configuration [ issue 49](https://github.com/itplr-kosit/validator-configuration-xrechnung/issues/49))
|
||||
- Remove saxon signature from java8 uber-jar (see [67](https://github.com/itplr-kosit/validator/issues/67))
|
||||
|
||||
## 1.4.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- date conversion when
|
||||
using [ConfigurationBuilder#date(Date)](https://github.com/itplr-kosit/validator/blob/d7beb1040418ae5cbeb9427532fd87482f55756c/src/main/java/de/kosit/validationtool/config/ConfigurationBuilder.java#L109)
|
||||
- (CLI) [#51](https://github.com/itplr-kosit/validator/issues/51) Suffix of report xml is missing
|
||||
- [#53](https://github.com/itplr-kosit/validator/issues/53) Fix copyright and licensing information
|
||||
- [#56](https://github.com/itplr-kosit/validator/issues/56) `namespace` element content needs trimming
|
||||
- [DAEMON] [#57](https://github.com/itplr-kosit/validator/issues/57) Reading large inputs correctly
|
||||
|
||||
### Added
|
||||
|
||||
- read saxon XdmNode with InputFactory
|
||||
- (CLI) custom output without the various log messages
|
||||
- (CLI) options to set the log level (`-X` = full debug output, `-l <level>` set a specific level)
|
||||
- (CLI) return code is not 0 on rejected results
|
||||
- (CLI) read (single) test target from stdin
|
||||
- [DAEMON] name inputs via request URI
|
||||
|
||||
### Changed
|
||||
|
||||
- InputFactory has methods to read any java.xml.transform.Source as Input not only StreamSources
|
||||
- InputFactory uses a generated UUID as name for SourceInput, if no "real" name can be derived
|
||||
- saxon dependency update (minor, 9.9.1-7)
|
||||
- [DAEMON] proper status codes when returning results (see [daemon documentation](https://github.com/itplr-kosit/validator/blob/master/docs/daemon.md#status-codes))
|
||||
|
||||
## 1.3.1
|
||||
|
||||
### Fixed
|
||||
|
||||
- `getFailedAsserts()` and `isSchematronValid()`
|
||||
in [DefaultResult.java](https://github.com/itplr-kosit/validator/blob/master/src/main/java/de/kosit/validationtool/impl/DefaultResult.java)
|
||||
do not reflect actual schematron validation result
|
||||
- processing aborts on schematron execution errors (e.g. errors within schematron logic). The validator now generates a
|
||||
report in such cases.
|
||||
- exception while resolving when using XSLT's `unparsed-text()` function within report generation
|
||||
|
||||
### Added
|
||||
|
||||
- (CLI) summary report
|
||||
|
||||
### Changed
|
||||
|
||||
- engine info contains version number of the validator (configurations can output this in the report for maintainance
|
||||
puposes)
|
||||
- options to customize serialized report file names (cmdline only) via `--report-prefix` and `--report-postfix`
|
||||
- remove unused dependency Apache Commons HTTP
|
||||
|
||||
## 1.3.0
|
||||
|
||||
### Added
|
||||
|
||||
- Added a builder style configuration API to configure scenarios
|
||||
- Added an option to configure xml security e.g. to load from http sources or not from a specific repository
|
||||
(so loading is configurable less restrictive, default strategy is to only load from a local repository)
|
||||
- Support java.xml.transform.Source as Input
|
||||
|
||||
### Changed
|
||||
|
||||
- Inputs are NOT read into memory (e.g. Byte-Array) prior processing within the validator. This reduces memory
|
||||
consumption.
|
||||
- Overall processing of xml files is based on Saxon s9api. No JAXP or SAX classes are used by
|
||||
the validator (this further improves performance and memory consumption)
|
||||
|
||||
### Deprecations
|
||||
|
||||
- CheckConfiguration is deprecated now. Use Configuration.load(...) or Configuration.build(...)
|
||||
|
||||
## 1.2.1
|
||||
|
||||
### Fixed
|
||||
|
||||
- Validator is creating invalid createReportInput xml in case of no scenario match
|
||||
|
||||
## 1.2.0
|
||||
|
||||
### Added
|
||||
|
||||
- Provide access to schematron result
|
||||
through [Result.java](https://github.com/itplr-kosit/validator/blob/master/src/main/java/de/kosit/validationtool/api/Result.java)
|
||||
- *Result#getFailedAsserts()* returns a list of failed asserts found by schematron
|
||||
- *Result#isSchematronValid()* convinience access to evaluate whether schematron was processed without any *
|
||||
FailedAsserts*
|
||||
|
||||
### Changed
|
||||
|
||||
- *Result#getAcceptRecommendation()* does not _only_ work when _acceptMatch_ is configured in the scenario
|
||||
- schema correctness is a precondition, if the checked instance is not valid, this evaluates to _REJECTED_
|
||||
- if _acceptMatch_ is configured, the result is based on the boolean result of the xpath expression evaluated against
|
||||
the generated report
|
||||
- if *no* _acceptMatch_ is configured, the result is based on evaluation of schema and schematron correctness
|
||||
- _UNDEFINED_ is only returned, when processing is stopped somehow
|
||||
- *Result#isAcceptable()* can now evaluate to true, when no _acceptMatch_ is configured (see above)
|
||||
|
||||
## 1.1.3
|
||||
|
||||
### Fixed
|
||||
|
||||
- XXE vulnerability when reading xml documents with Saxon [#44](https://github.com/itplr-kosit/validator/issues/44)
|
||||
- validator unintentionally stopped when schematron processing has errors.
|
||||
See [#41](https://github.com/itplr-kosit/validator/issues/41).
|
||||
|
||||
## 1.1.2
|
||||
|
||||
### Fixed
|
||||
|
||||
- NPE in Result.getReportDocument for malformed xml input
|
||||
|
||||
## 1.1.1
|
||||
|
||||
### Added
|
||||
|
||||
- Convenience method for accessing information about well-formedness in Result
|
||||
- Convenience method for accessing information about schema validation result in Result
|
||||
|
||||
### Fixed
|
||||
|
||||
- NPE when validating non-XML files
|
||||
|
||||
## 1.1.0
|
||||
|
||||
### Added
|
||||
|
||||
- Enhanced API-Usage e.g. return _Result_ object with processing information
|
||||
- Support loading scenarios and content from a JAR-File
|
||||
- Simple Daemon-Mode exposing validation functionality via http
|
||||
- cli option to serialize the 'report input' xml document to _cwd_ (current working directory)
|
||||
- Documentation in `docs` folder
|
||||
|
||||
### Changed
|
||||
|
||||
- Use s9api (e.g. XdmNode) internally for loading and holding xml objects (further memory optimization)
|
||||
- Builds with java 8 and >= 11
|
||||
- Packages for java8 and java >= 11 (with jaxb included)
|
||||
- Translated README.md
|
||||
|
||||
## 1.0.2
|
||||
|
||||
### Fixed
|
||||
|
||||
- Memory issues when validating multiple targets
|
||||
|
||||
## 1.0.1
|
||||
|
||||
### Changed
|
||||
|
||||
- Removed XRechnung configuration from release artifacts and source (moved
|
||||
to [own repository](https://github.com/itplr-kosit/validator-configuration-xrechnung) )
|
||||
|
||||
## 1.0.0
|
||||
|
||||
- Initial Release
|
||||
20
server/ui/docs/configurations.md
Normal file
20
server/ui/docs/configurations.md
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# Configurations
|
||||
|
||||
The validator needs a scenario configuration for working properly.
|
||||
|
||||
Currently, there are two public third party validation configurations available.
|
||||
|
||||
* Validation Configuration for [XRechnung](http://www.xoev.de/de/xrechnung):
|
||||
* Source code is available on [GitHub](https://github.com/itplr-kosit/validator-configuration-xrechnung)
|
||||
* [Releases](https://github.com/itplr-kosit/validator-configuration-xrechnung/releases) can also be downloaded
|
||||
* Validation Configuration for [XGewerbeanzeige](https://xgewerbeanzeige.de/)
|
||||
* Source code is available on [GitHub](https://github.com/itplr-kosit/validator-configuration-xgewerbeanzeige)
|
||||
* [Releases](https://github.com/itplr-kosit/validator-configuration-xgewerbeanzeige/releases) can also be downloaded
|
||||
|
||||
For creating custom configurations
|
||||
see [configuration documentation](https://github.com/itplr-kosit/validator/blob/master/docs/configurations.md)
|
||||
for details
|
||||
184
server/ui/docusaurus.config.js
Normal file
184
server/ui/docusaurus.config.js
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
/*
|
||||
* Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// @ts-check
|
||||
// Note: type annotations allow type checking and IDEs autocompletion
|
||||
|
||||
const lightCodeTheme = require("prism-react-renderer/themes/github");
|
||||
const darkCodeTheme = require("prism-react-renderer/themes/dracula");
|
||||
const pkg = require("./package.json");
|
||||
|
||||
/** @type {import('@docusaurus/types').Config} */
|
||||
const config = {
|
||||
title: "KoSIT Validator Daemon",
|
||||
tagline: "Validating any XML",
|
||||
url: "https://your-docusaurus-test-site.com",
|
||||
baseUrl: "/",
|
||||
onBrokenLinks: "throw",
|
||||
onBrokenMarkdownLinks: "warn",
|
||||
favicon: "img/favicon.svg",
|
||||
customFields: {
|
||||
// We fake a base endpoint here, so that our proxy works in development mode.
|
||||
// It does not seem to work when trying to proxy requests to the root, so we
|
||||
// need to create a base path to proxy against
|
||||
apiBase: process.env.NODE_ENV === "development" ? "/api" : "/",
|
||||
},
|
||||
|
||||
// GitHub pages deployment config.
|
||||
// If you aren't using GitHub pages, you don't need these.
|
||||
organizationName: "KoSIT", // Usually your GitHub org/user name.
|
||||
projectName: "Validator", // Usually your repo name.
|
||||
|
||||
// Even if you don't use internalization, you can use this field to set useful
|
||||
// metadata like html lang. For example, if your site is Chinese, you may want
|
||||
// to replace "en" with "zh-Hans".
|
||||
i18n: {
|
||||
defaultLocale: "en",
|
||||
locales: ["en"],
|
||||
},
|
||||
|
||||
presets: [
|
||||
[
|
||||
"classic",
|
||||
/** @type {import('@docusaurus/preset-classic').Options} */
|
||||
({
|
||||
docs: {
|
||||
sidebarPath: require.resolve("./sidebars.js"),
|
||||
// Please change this to your repo.
|
||||
// Remove this to remove the "edit this page" links.
|
||||
editUrl: "https://github.com/itplr-kosit/validator/server/ui",
|
||||
},
|
||||
theme: {
|
||||
customCss: require.resolve("./src/css/custom.css"),
|
||||
},
|
||||
}),
|
||||
],
|
||||
],
|
||||
|
||||
themeConfig:
|
||||
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
|
||||
({
|
||||
navbar: {
|
||||
style: "primary",
|
||||
title: "Validator Daemon",
|
||||
logo: {
|
||||
alt: "KoSIT Validator Daemon",
|
||||
src: "img/logo.svg",
|
||||
},
|
||||
items: [
|
||||
{
|
||||
type: "doc",
|
||||
docId: "api",
|
||||
position: "left",
|
||||
label: "Documentation",
|
||||
},
|
||||
{
|
||||
to: "config",
|
||||
position: "left",
|
||||
label: "Validator configuration",
|
||||
},
|
||||
{
|
||||
to: "health",
|
||||
position: "left",
|
||||
label: "Health information",
|
||||
},
|
||||
],
|
||||
},
|
||||
footer: {
|
||||
style: "dark",
|
||||
links: [
|
||||
{
|
||||
title: "Docs",
|
||||
items: [
|
||||
{
|
||||
label: "Configuration",
|
||||
to: "/docs/configurations",
|
||||
},
|
||||
{
|
||||
label: "API",
|
||||
to: "/docs/api",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
title: "Community",
|
||||
items: [
|
||||
{
|
||||
label: "Github",
|
||||
href: "https://github.com/itplr-kosit/validator",
|
||||
},
|
||||
{
|
||||
label: "Issues",
|
||||
href: "https://github.com/itplr-kosit/validator/issues",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "More",
|
||||
items: [
|
||||
{
|
||||
label: "KoSIT",
|
||||
href: "https://www.xoev.de",
|
||||
},
|
||||
{
|
||||
label: "XRechnung",
|
||||
href: "https://www.xoev.de/xrechnung-16828",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
copyright: `Copyright © ${new Date().getFullYear()} Koordinierungstelle für IT-Standards (KoSIT)`,
|
||||
},
|
||||
prism: {
|
||||
theme: lightCodeTheme,
|
||||
darkTheme: darkCodeTheme,
|
||||
},
|
||||
}),
|
||||
|
||||
plugins: [
|
||||
/** @type {import('@docusaurus/types').PluginModule} */
|
||||
(
|
||||
// For the development environment to work, we need to proxy all requests
|
||||
// that are not meant to fetch static content (js, css, html files), to
|
||||
// the backend server. The dev server makes tht a little hard for us, as
|
||||
// it does not allow us to just proxy all requests that don't match any
|
||||
// static file. That's why we prefix every request with `/api`, and remove
|
||||
// it again when forwarding the request. In ptoduction mode, the endpoint
|
||||
// will be just `/` (see `config.customFields.apiBase`)
|
||||
function proxyPlugin() {
|
||||
return {
|
||||
name: "custom-docusaurus-plugin",
|
||||
configureWebpack() {
|
||||
return {
|
||||
devServer: {
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: pkg.apiProxy,
|
||||
pathRewrite: { "^/api": "" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
25115
server/ui/package-lock.json
generated
Normal file
25115
server/ui/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
68
server/ui/package.json
Normal file
68
server/ui/package.json
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
{
|
||||
"name": "validator-frontend",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"apiProxy": "http://localhost:8080",
|
||||
"scripts": {
|
||||
"docusaurus": "docusaurus",
|
||||
"start": "cross-env NODE_ENV=development docusaurus start",
|
||||
"build": "docusaurus build",
|
||||
"swizzle": "docusaurus swizzle",
|
||||
"deploy": "docusaurus deploy",
|
||||
"clear": "docusaurus clear",
|
||||
"serve": "docusaurus serve",
|
||||
"write-translations": "docusaurus write-translations",
|
||||
"write-heading-ids": "docusaurus write-heading-ids",
|
||||
"typecheck": "tsc",
|
||||
"checkFormatting": "prettier . --check",
|
||||
"format": "prettier . --write",
|
||||
"lint": "eslint . && npm run checkFormatting",
|
||||
"lint:fix": "npm run format && eslint . --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "2.2.0",
|
||||
"@docusaurus/preset-classic": "2.2.0",
|
||||
"@mdx-js/react": "^1.6.22",
|
||||
"@mui/icons-material": "^5.10.14",
|
||||
"clsx": "^1.2.1",
|
||||
"js-file-download": "^0.4.12",
|
||||
"prism-react-renderer": "^1.3.5",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-dropzone": "^14.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "2.2.0",
|
||||
"@tsconfig/docusaurus": "^1.0.5",
|
||||
"@typescript-eslint/eslint-plugin": "^5.43.0",
|
||||
"@typescript-eslint/parser": "^5.43.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.27.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.2",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.6.1",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.31.10",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"prettier": "^2.7.1",
|
||||
"typescript": "^4.7.4"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
"> 0.5%",
|
||||
"last 2 versions",
|
||||
"Firefox ESR",
|
||||
"not dead"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.14"
|
||||
}
|
||||
}
|
||||
33
server/ui/sidebars.js
Normal file
33
server/ui/sidebars.js
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
/**
|
||||
* Creating a sidebar enables you to:
|
||||
- create an ordered group of docs
|
||||
- render a sidebar for each doc of that group
|
||||
- provide next/previous navigation
|
||||
|
||||
The sidebars can be generated from the filesystem, or explicitly defined here.
|
||||
|
||||
Create as many sidebars as you want.
|
||||
*/
|
||||
|
||||
// @ts-check
|
||||
|
||||
/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
|
||||
const sidebars = {
|
||||
// By default, Docusaurus generates a sidebar from the docs folder structure
|
||||
tutorialSidebar: [{ type: "autogenerated", dirName: "." }],
|
||||
|
||||
// But you can create a sidebar manually
|
||||
/*
|
||||
tutorialSidebar: [
|
||||
'intro',
|
||||
'hello',
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Tutorial',
|
||||
items: ['tutorial-basics/create-a-document'],
|
||||
},
|
||||
],
|
||||
*/
|
||||
};
|
||||
|
||||
module.exports = sidebars;
|
||||
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.
|
||||
0
server/ui/static/.nojekyll
Normal file
0
server/ui/static/.nojekyll
Normal file
BIN
server/ui/static/img/docusaurus.png
Normal file
BIN
server/ui/static/img/docusaurus.png
Normal file
Binary file not shown.
3
server/ui/static/img/favicon.svg
Normal file
3
server/ui/static/img/favicon.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 24 24">
|
||||
<path fill="hsl(220, 41%, 30%)" d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm-3.06 16L7.4 14.46l1.41-1.41 2.12 2.12 4.24-4.24 1.41 1.41L10.94 18zM13 9V3.5L18.5 9H13z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 306 B |
3
server/ui/static/img/logo.svg
Normal file
3
server/ui/static/img/logo.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 24 24">
|
||||
<path fill="#fff" d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm-3.06 16L7.4 14.46l1.41-1.41 2.12 2.12 4.24-4.24 1.41 1.41L10.94 18zM13 9V3.5L18.5 9H13z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 292 B |
9
server/ui/tsconfig.json
Normal file
9
server/ui/tsconfig.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
// This file is not used in compilation. It is here just for a nice editor experience.
|
||||
"extends": "@tsconfig/docusaurus/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"resolveJsonModule": true,
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue