+ Token successfully created
+ The API token {p.token.name} was successfully created. Please
+ note the following information as they won't be available after.
+
+
+ API URL:
+
+ Token ID:
+
+ Token secret:
+
+
+
+ );
+}
diff --git a/matrixgw_frontend/src/utils/DateUtils.ts b/matrixgw_frontend/src/utils/DateUtils.ts
new file mode 100644
index 0000000..c44c16f
--- /dev/null
+++ b/matrixgw_frontend/src/utils/DateUtils.ts
@@ -0,0 +1,8 @@
+/**
+ * Get UNIX time
+ *
+ * @returns Number of seconds since Epoch
+ */
+export function time(): number {
+ return Math.floor(new Date().getTime() / 1000);
+}
diff --git a/matrixgw_frontend/src/utils/FormUtils.ts b/matrixgw_frontend/src/utils/FormUtils.ts
new file mode 100644
index 0000000..e47e9ec
--- /dev/null
+++ b/matrixgw_frontend/src/utils/FormUtils.ts
@@ -0,0 +1,52 @@
+import isCidr from "is-cidr";
+import type { LenConstraint } from "../api/ServerApi";
+
+/**
+ * Check if a constraint was respected or not
+ *
+ * @returns An error message appropriate for the constraint
+ * violation, if any, or undefined otherwise
+ */
+export function checkConstraint(
+ constraint: LenConstraint,
+ value: string | undefined
+): string | undefined {
+ value = value ?? "";
+ if (value.length < constraint.min)
+ return `Please specify at least ${constraint.min} characters!`;
+
+ if (value.length > constraint.max)
+ return `Please specify at least ${constraint.min} characters!`;
+
+ return undefined;
+}
+
+/**
+ * Check if a number constraint was respected or not
+ *
+ * @returns An error message appropriate for the constraint
+ * violation, if any, or undefined otherwise
+ */
+export function checkNumberConstraint(
+ constraint: LenConstraint,
+ value: number
+): string | undefined {
+ value = value ?? "";
+ if (value < constraint.min)
+ return `Value is below accepted minimum (${constraint.min})!`;
+
+ if (value > constraint.max)
+ return `Value is above accepted maximum (${constraint.min})!`;
+
+ return undefined;
+}
+
+/**
+ * Check whether a given IP network address is valid or not
+ *
+ * @param ip The IP network to check
+ * @returns true if the address is valid, false otherwise
+ */
+export function isIPNetworkValid(ip: string): boolean {
+ return isCidr(ip) !== 0;
+}
diff --git a/matrixgw_frontend/src/widgets/CopyTextChip.tsx b/matrixgw_frontend/src/widgets/CopyTextChip.tsx
new file mode 100644
index 0000000..864ef58
--- /dev/null
+++ b/matrixgw_frontend/src/widgets/CopyTextChip.tsx
@@ -0,0 +1,29 @@
+import { Chip, Tooltip } from "@mui/material";
+import { useAlert } from "../hooks/contexts_provider/AlertDialogProvider";
+import { useSnackbar } from "../hooks/contexts_provider/SnackbarProvider";
+
+export function CopyTextChip(p: { text: string }): React.ReactElement {
+ const snackbar = useSnackbar();
+ const alert = useAlert();
+
+ const copyTextToClipboard = () => {
+ try {
+ navigator.clipboard.writeText(p.text);
+ snackbar(`'${p.text}' was copied to clipboard.`);
+ } catch (e) {
+ console.error(`Failed to copy text to the clipboard! ${e}`);
+ alert(p.text);
+ }
+ };
+
+ return (
+
+
+
+ );
+}
diff --git a/matrixgw_frontend/src/widgets/forms/CheckboxInput.tsx b/matrixgw_frontend/src/widgets/forms/CheckboxInput.tsx
new file mode 100644
index 0000000..dcf7c71
--- /dev/null
+++ b/matrixgw_frontend/src/widgets/forms/CheckboxInput.tsx
@@ -0,0 +1,23 @@
+import { Checkbox, FormControlLabel } from "@mui/material";
+
+export function CheckboxInput(p: {
+ editable: boolean;
+ label: string;
+ checked: boolean | undefined;
+ onValueChange: (v: boolean) => void;
+}): React.ReactElement {
+ return (
+ {
+ p.onValueChange(e.target.checked);
+ }}
+ />
+ }
+ label={p.label}
+ />
+ );
+}
diff --git a/matrixgw_frontend/src/widgets/forms/DateInput.tsx b/matrixgw_frontend/src/widgets/forms/DateInput.tsx
new file mode 100644
index 0000000..c186e14
--- /dev/null
+++ b/matrixgw_frontend/src/widgets/forms/DateInput.tsx
@@ -0,0 +1,49 @@
+import { DateField } from "@mui/x-date-pickers";
+import dayjs from "dayjs";
+import { TextInput } from "./TextInput";
+
+export function DateInput(p: {
+ editable?: boolean;
+ required?: boolean;
+ label: string;
+ value: number | undefined | null;
+ checkValue?: (s: number) => boolean;
+ disableFuture?: boolean;
+ disablePast?: boolean;
+ onChange: (newVal: number | undefined | null) => void;
+}): React.ReactElement {
+ const date = p.value ? dayjs.unix(p.value) : undefined;
+
+ const error = p.value && p.checkValue && !p.checkValue(p.value);
+
+ if (!p.editable)
+ return (
+
+ );
+
+ return (
+ p.onChange(v?.unix())}
+ slotProps={{
+ textField: {
+ fullWidth: true,
+ label: p.label,
+ variant: "standard",
+ },
+ inputAdornment: {
+ variant: "standard",
+ },
+ }}
+ disableFuture={p.disableFuture}
+ disablePast={p.disablePast}
+ error={error === true}
+ format="DD/MM/YYYY"
+ />
+ );
+}
diff --git a/matrixgw_frontend/src/widgets/forms/NetworksInput.tsx b/matrixgw_frontend/src/widgets/forms/NetworksInput.tsx
new file mode 100644
index 0000000..a583196
--- /dev/null
+++ b/matrixgw_frontend/src/widgets/forms/NetworksInput.tsx
@@ -0,0 +1,26 @@
+import { isIPNetworkValid } from "../../utils/FormUtils";
+import { TextInput } from "./TextInput";
+
+function rebuildNetworksList(val?: string): string[] | undefined {
+ if (!val || val.trim() === "") return undefined;
+
+ return val.split(",").map((v) => v.trim());
+}
+
+export function NetworksInput(p: {
+ editable?: boolean;
+ label: string;
+ value?: string[];
+ onChange: (n: string[] | undefined) => void;
+}): React.ReactElement {
+ const textValue = (p.value ?? []).join(", ").trim();
+ return (
+ p.onChange(rebuildNetworksList(i))}
+ checkValue={(v) => (rebuildNetworksList(v) ?? []).every(isIPNetworkValid)}
+ />
+ );
+}
diff --git a/matrixgw_frontend/src/widgets/forms/TextInput.tsx b/matrixgw_frontend/src/widgets/forms/TextInput.tsx
new file mode 100644
index 0000000..8b461a8
--- /dev/null
+++ b/matrixgw_frontend/src/widgets/forms/TextInput.tsx
@@ -0,0 +1,65 @@
+import { TextField, type TextFieldVariants } from "@mui/material";
+import type { LenConstraint } from "../../api/ServerApi";
+
+/**
+ * Text input
+ */
+export function TextInput(p: {
+ label?: string;
+ editable?: boolean;
+ required?: boolean;
+ value?: string;
+ onValueChange?: (newVal: string | undefined) => void;
+ size?: LenConstraint;
+ checkValue?: (s: string) => boolean;
+ multiline?: boolean;
+ minRows?: number;
+ maxRows?: number;
+ placeholder?: string;
+ type?: React.HTMLInputTypeAttribute;
+ style?: React.CSSProperties;
+ helperText?: string;
+ variant?: TextFieldVariants;
+}): React.ReactElement {
+ if (!p.editable && (p.value ?? "") === "") return <>>;
+
+ let valueError = undefined;
+ if (p.value && p.value.length > 0) {
+ if (p.size?.min && p.type !== "number" && p.value.length < p.size.min)
+ valueError = `Please specify at least ${p.size.min} characters !`;
+ if (p.checkValue && !p.checkValue(p.value)) valueError = "Invalid value!";
+ if (
+ p.type === "number" &&
+ p.size &&
+ (Number(p.value) > p.size.max || Number(p.value) < p.size.min)
+ )
+ valueError = "Invalid size range!";
+ }
+
+ return (
+
+ p.onValueChange?.(
+ e.target.value.length === 0 ? undefined : e.target.value
+ )
+ }
+ slotProps={{
+ input: {
+ readOnly: !p.editable,
+ type: p.type,
+ },
+ htmlInput: { maxLength: p.size?.max, placeholder: p.placeholder },
+ }}
+ variant={p.variant ?? "standard"}
+ style={p.style ?? { width: "100%", marginBottom: "15px" }}
+ multiline={p.multiline}
+ minRows={p.minRows}
+ maxRows={p.maxRows}
+ error={valueError !== undefined}
+ helperText={valueError ?? p.helperText}
+ />
+ );
+}