aboutsummaryrefslogtreecommitdiffhomepage
path: root/backend/private
diff options
context:
space:
mode:
authorBruce Berrios <[email protected]>2022-06-07 08:06:15 -0400
committerGitHub <[email protected]>2022-06-07 14:06:15 +0200
commitf2998b1d28c9334ba50f9909db94bed363375e6f (patch)
tree3b806591f7c90c9002dea0e8618d50046f1499af /backend/private
parent5ed1f166dd56a8b022754d45bf41ce6487ea20fb (diff)
downloadmonkeytype-f2998b1d28c9334ba50f9909db94bed363375e6f.tar.gz
monkeytype-f2998b1d28c9334ba50f9909db94bed363375e6f.zip
Add server configuration panel (#3070) bruception
* Add server configuration panel * Remove unnecessaary check * Remove break * styling changes showing when configuration was saved * changing color based on response * Remove comment * Changes * Add support for arrays * Arbitrary nesting * Add array item controls * added button to quickly open the configuration panel * removed excessive padding * text inputs same height and style as checkboxes * monkey stylng Co-authored-by: Miodec <[email protected]>
Diffstat (limited to 'backend/private')
-rw-r--r--backend/private/index.html60
-rw-r--r--backend/private/script.js251
-rw-r--r--backend/private/style.css187
3 files changed, 498 insertions, 0 deletions
diff --git a/backend/private/index.html b/backend/private/index.html
new file mode 100644
index 000000000..fd4f83131
--- /dev/null
+++ b/backend/private/index.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width" />
+ <title>API Server Configuration</title>
+ <link href="style.css" rel="stylesheet" type="text/css" />
+ </head>
+
+ <body>
+ <div id="header">
+ <div class="header-container">
+ <div id="logo">
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ style="isolation: isolate"
+ viewBox="-680 -1030 300 180"
+ >
+ <g>
+ <path
+ d="M -430 -910 L -430 -910 C -424.481 -910 -420 -905.519 -420 -900 L -420 -900 C -420 -894.481 -424.481 -890 -430 -890 L -430 -890 C -435.519 -890 -440 -894.481 -440 -900 L -440 -900 C -440 -905.519 -435.519 -910 -430 -910 Z"
+ ></path>
+ <path
+ d=" M -570 -910 L -510 -910 C -504.481 -910 -500 -905.519 -500 -900 L -500 -900 C -500 -894.481 -504.481 -890 -510 -890 L -570 -890 C -575.519 -890 -580 -894.481 -580 -900 L -580 -900 C -580 -905.519 -575.519 -910 -570 -910 Z "
+ ></path>
+ <path
+ d="M -590 -970 L -590 -970 C -584.481 -970 -580 -965.519 -580 -960 L -580 -940 C -580 -934.481 -584.481 -930 -590 -930 L -590 -930 C -595.519 -930 -600 -934.481 -600 -940 L -600 -960 C -600 -965.519 -595.519 -970 -590 -970 Z"
+ ></path>
+ <path
+ d=" M -639.991 -960.515 C -639.72 -976.836 -626.385 -990 -610 -990 L -610 -990 C -602.32 -990 -595.31 -987.108 -590 -982.355 C -584.69 -987.108 -577.68 -990 -570 -990 L -570 -990 C -553.615 -990 -540.28 -976.836 -540.009 -960.515 C -540.001 -960.345 -540 -960.172 -540 -960 L -540 -960 L -540 -940 C -540 -934.481 -544.481 -930 -550 -930 L -550 -930 C -555.519 -930 -560 -934.481 -560 -940 L -560 -960 L -560 -960 C -560 -965.519 -564.481 -970 -570 -970 C -575.519 -970 -580 -965.519 -580 -960 L -580 -960 L -580 -960 L -580 -940 C -580 -934.481 -584.481 -930 -590 -930 L -590 -930 C -595.519 -930 -600 -934.481 -600 -940 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 C -600 -965.519 -604.481 -970 -610 -970 C -615.519 -970 -620 -965.519 -620 -960 L -620 -960 L -620 -940 C -620 -934.481 -624.481 -930 -630 -930 L -630 -930 C -635.519 -930 -640 -934.481 -640 -940 L -640 -960 L -640 -960 C -640 -960.172 -639.996 -960.344 -639.991 -960.515 Z "
+ ></path>
+ <path
+ d=" M -460 -930 L -460 -900 C -460 -894.481 -464.481 -890 -470 -890 L -470 -890 C -475.519 -890 -480 -894.481 -480 -900 L -480 -930 L -508.82 -930 C -514.99 -930 -520 -934.481 -520 -940 L -520 -940 C -520 -945.519 -514.99 -950 -508.82 -950 L -431.18 -950 C -425.01 -950 -420 -945.519 -420 -940 L -420 -940 C -420 -934.481 -425.01 -930 -431.18 -930 L -460 -930 Z "
+ ></path>
+ <path
+ d="M -470 -990 L -430 -990 C -424.481 -990 -420 -985.519 -420 -980 L -420 -980 C -420 -974.481 -424.481 -970 -430 -970 L -470 -970 C -475.519 -970 -480 -974.481 -480 -980 L -480 -980 C -480 -985.519 -475.519 -990 -470 -990 Z"
+ ></path>
+ <path
+ d=" M -630 -910 L -610 -910 C -604.481 -910 -600 -905.519 -600 -900 L -600 -900 C -600 -894.481 -604.481 -890 -610 -890 L -630 -890 C -635.519 -890 -640 -894.481 -640 -900 L -640 -900 C -640 -905.519 -635.519 -910 -630 -910 Z "
+ ></path>
+ <path
+ d=" M -515 -990 L -510 -990 C -504.481 -990 -500 -985.519 -500 -980 L -500 -980 C -500 -974.481 -504.481 -970 -510 -970 L -515 -970 C -520.519 -970 -525 -974.481 -525 -980 L -525 -980 C -525 -985.519 -520.519 -990 -515 -990 Z "
+ ></path>
+ <path
+ d=" M -660 -910 L -680 -910 L -680 -980 C -680 -1007.596 -657.596 -1030 -630 -1030 L -430 -1030 C -402.404 -1030 -380 -1007.596 -380 -980 L -380 -900 C -380 -872.404 -402.404 -850 -430 -850 L -630 -850 C -657.596 -850 -680 -872.404 -680 -900 L -680 -920 L -660 -920 L -660 -900 C -660 -883.443 -646.557 -870 -630 -870 L -430 -870 C -413.443 -870 -400 -883.443 -400 -900 L -400 -980 C -400 -996.557 -413.443 -1010 -430 -1010 L -630 -1010 C -646.557 -1010 -660 -996.557 -660 -980 L -660 -910 Z "
+ ></path>
+ </g>
+ </svg>
+ </div>
+ <h1>API Server Configuration</h1>
+ </div>
+ </div>
+ <div id="root">
+ <span id="form-loader" class="loader"></span>
+ </div>
+ <div id="save">Save Changes</div>
+ <script src="script.js"></script>
+ </body>
+</html>
diff --git a/backend/private/script.js b/backend/private/script.js
new file mode 100644
index 000000000..e2fa1d77e
--- /dev/null
+++ b/backend/private/script.js
@@ -0,0 +1,251 @@
+let state = {};
+let schema = {};
+
+const buildLabel = (elementType, text) => {
+ const labelElement = document.createElement("label");
+ labelElement.innerHTML = text;
+ labelElement.style.fontWeight = elementType === "group" ? "bold" : "lighter";
+
+ return labelElement;
+};
+
+const buildNumberInput = (schema, parentState, key) => {
+ const input = document.createElement("input");
+ input.classList.add("base-input");
+ input.type = "number";
+ input.value = parentState[key];
+ input.min = schema.min || 0;
+
+ input.addEventListener("change", () => {
+ const normalizedValue = parseFloat(input.value, 10);
+ parentState[key] = normalizedValue;
+ });
+
+ return input;
+};
+
+const buildBooleanInput = (parentState, key) => {
+ const input = document.createElement("input");
+ input.classList.add("base-input");
+ input.type = "checkbox";
+ input.checked = parentState[key] || false;
+
+ input.addEventListener("change", () => {
+ parentState[key] = input.checked;
+ });
+
+ return input;
+};
+
+const buildStringInput = (parentState, key) => {
+ const input = document.createElement("input");
+ input.classList.add("base-input");
+ input.type = "text";
+ input.value = parentState[key] || "";
+
+ input.addEventListener("change", () => {
+ parentState[key] = input.value;
+ });
+
+ return input;
+};
+
+const defaultValueForType = (type) => {
+ switch (type) {
+ case "number":
+ return 0;
+ case "boolean":
+ return false;
+ case "string":
+ return "";
+ case "array":
+ return [];
+ case "object":
+ return {};
+ }
+
+ return null;
+};
+
+const arrayFormElementDecorator = (childElement, parentState, index) => {
+ const decoratedElement = document.createElement("div");
+ decoratedElement.classList.add("array-form-element-decorator");
+
+ const removeButton = document.createElement("button");
+ removeButton.innerHTML = "X";
+ removeButton.classList.add("array-input", "array-input-delete", "button");
+ removeButton.addEventListener("click", () => {
+ parentState.splice(index, 1);
+ rerender();
+ });
+
+ decoratedElement.appendChild(childElement);
+ decoratedElement.appendChild(removeButton);
+
+ return decoratedElement;
+};
+
+const buildArrayInput = (schema, parentState) => {
+ const itemType = schema.items.type;
+ const inputControlsDiv = document.createElement("div");
+ inputControlsDiv.classList.add("array-input-controls");
+
+ const addButton = document.createElement("button");
+ addButton.innerHTML = "Add One";
+ addButton.classList.add("array-input", "button");
+ addButton.addEventListener("click", () => {
+ parentState.push(defaultValueForType(itemType));
+ rerender();
+ });
+
+ const removeButton = document.createElement("button");
+ removeButton.innerHTML = "Delete All";
+ removeButton.classList.add("array-input", "array-input-delete", "button");
+ removeButton.addEventListener("click", () => {
+ parentState.splice(0, parentState.length);
+ rerender();
+ });
+
+ inputControlsDiv.appendChild(addButton);
+ inputControlsDiv.appendChild(removeButton);
+
+ return inputControlsDiv;
+};
+
+const buildUnknownInput = () => {
+ const disclaimer = document.createElement("div");
+ disclaimer.innerHTML = `<i class="unknown-input">This configuration is not yet supported</i>`;
+
+ return disclaimer;
+};
+
+const render = (state, schema) => {
+ const build = (
+ schema,
+ state,
+ parentState,
+ currentKey = "",
+ path = "configuration"
+ ) => {
+ const parent = document.createElement("div");
+ parent.classList.add("form-element");
+
+ const { type, label, fields, items } = schema;
+
+ if (label) {
+ parent.appendChild(buildLabel(type, label));
+ }
+
+ parent.id = path;
+
+ if (type === "object") {
+ const entries = Object.entries(fields);
+ entries.forEach(([key, value]) => {
+ if (!state[key]) {
+ state[key] = defaultValueForType(value.type);
+ }
+
+ const childElement = build(
+ value,
+ state[key],
+ state,
+ key,
+ `${path}.${key}`
+ );
+ parent.appendChild(childElement);
+ });
+ } else if (type === "array") {
+ const arrayInputControls = buildArrayInput(schema, state);
+ parent.appendChild(arrayInputControls);
+
+ if (state && state.length > 0) {
+ state.forEach((element, index) => {
+ const childElement = build(
+ items,
+ element,
+ state,
+ `${currentKey}[${index}]`,
+ `${path}[${index}]`
+ );
+
+ const decoratedChildElement = arrayFormElementDecorator(
+ childElement,
+ state,
+ index
+ );
+ parent.appendChild(decoratedChildElement);
+ });
+ }
+ } else if (type === "number") {
+ parent.appendChild(buildNumberInput(schema, parentState, currentKey));
+ parent.classList.add("input-label");
+ } else if (type === "string") {
+ parent.appendChild(buildStringInput(parentState, currentKey));
+ parent.classList.add("input-label");
+ } else if (type === "boolean") {
+ parent.appendChild(buildBooleanInput(parentState, currentKey));
+ parent.classList.add("input-label");
+ } else {
+ parent.appendChild(buildUnknownInput());
+ }
+
+ return parent;
+ };
+
+ return build(schema, state, state);
+};
+
+function rerender() {
+ const root = document.querySelector("#root");
+ root.innerHTML = "";
+ root?.append(render(state, schema));
+}
+
+window.onload = async () => {
+ const schemaResponse = await fetch("/configuration/schema");
+ const dataResponse = await fetch("/configuration");
+
+ const schemaResponseJson = await schemaResponse.json();
+ const dataResponseJson = await dataResponse.json();
+
+ const { data: formSchema } = schemaResponseJson;
+ const { data: initialData } = dataResponseJson;
+
+ state = initialData;
+ schema = formSchema;
+
+ rerender();
+
+ const saveButton = document.querySelector("#save");
+
+ saveButton?.addEventListener("click", async () => {
+ if (saveButton.disabled) {
+ return;
+ }
+
+ saveButton.innerHTML = "Saving...";
+ saveButton.disabled = true;
+ const response = await fetch("/configuration", {
+ method: "PATCH",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ configuration: state,
+ }),
+ });
+ if (response.status === 200) {
+ saveButton.innerHTML = "Saved!";
+ saveButton.classList.add("good");
+ } else {
+ saveButton.innerHTML = "Failed!";
+ saveButton.classList.add("bad");
+ }
+ setTimeout(() => {
+ saveButton.innerHTML = "Save Changes";
+ saveButton.classList.remove("good");
+ saveButton.classList.remove("bad");
+ saveButton.disabled = false;
+ }, 3000);
+ });
+};
diff --git a/backend/private/style.css b/backend/private/style.css
new file mode 100644
index 000000000..e28298aec
--- /dev/null
+++ b/backend/private/style.css
@@ -0,0 +1,187 @@
+@import url("https://fonts.googleapis.com/css2?family=Roboto+Mono&display=swap");
+
+:root {
+ --roundness: 0.5rem;
+ /* --sub-color: #e1e1e1; */
+ /* --highlight-color: #0085a8; */
+ --bg-color: #323437;
+ --main-color: #e2b714;
+ --caret-color: #e2b714;
+ --sub-color: #646669;
+ --sub-alt-color: #2c2e31;
+ --text-color: #d1d0c5;
+ --error-color: #ca4754;
+}
+
+body {
+ font-family: "Roboto Mono", sans-serif;
+ margin: 0;
+ padding: 0;
+ background: var(--bg-color);
+}
+
+#header {
+ color: #fff;
+}
+
+.header-container {
+ padding: 1rem 0;
+ max-width: 60rem;
+ margin: 0 auto;
+ display: flex;
+}
+
+#logo {
+ align-items: center;
+ background-color: transparent;
+ display: grid;
+ width: 3rem;
+ margin-right: 1rem;
+}
+
+#header h1 {
+ font-size: 1.5rem;
+}
+
+#logo path {
+ fill: var(--main-color);
+}
+
+#root {
+ padding: 2rem;
+ background-color: #fff;
+ max-width: 60rem;
+ margin: 0rem auto;
+ border-radius: var(--roundness);
+}
+
+.button {
+ background-color: var(--bg-color);
+ color: #fff;
+ padding: 0.5rem 1rem;
+ border-radius: 0.25rem;
+ border: none;
+ cursor: pointer;
+ font-size: 1rem;
+ margin-right: 1rem;
+ font-family: "Roboto Mono";
+}
+
+.array-input {
+ margin: 1rem auto;
+}
+
+.array-input-delete {
+ margin-left: 1rem;
+ background-color: #d84b4b;
+}
+
+#save {
+ position: fixed;
+ right: 6rem;
+ bottom: 3rem;
+ background-color: var(--sub-alt-color);
+ color: var(--text-color);
+ font-style: bold;
+ border-radius: 3px;
+ padding: 1rem 2rem;
+ cursor: pointer;
+ display: inline-block;
+ width: 125px;
+ text-align: center;
+ transition: 0.125s;
+}
+
+#save:hover {
+ background-color: var(--text-color);
+ color: var(--bg-color);
+}
+
+#save.good {
+ background-color: var(--main-color);
+ color: var(--bg-color);
+}
+
+#save.bad {
+ background-color: var(--error-color);
+ color: var(--bg-color);
+}
+
+label {
+ display: block;
+}
+
+.base-input {
+ margin: 0.5rem;
+ border: 1px solid #767676;
+ border-radius: calc(var(--roundness) / 2);
+ background-color: #fff;
+ transition: all 0.2s ease-in-out;
+ font-size: 1rem;
+ padding: 0.25rem;
+ font-family: "Roboto Mono";
+}
+
+input[type="checkbox"] {
+ display: inline-block;
+ accent-color: var(--main-color);
+ color: white;
+ width: 1.5rem;
+ height: 1.5rem;
+}
+
+.form-element {
+ padding-left: 3rem;
+ padding-top: 0.25rem;
+ padding-bottom: 0.25rem;
+ border-left: var(--sub-color) 0.5px solid;
+ /* border-bottom: var(--sub-color) 0.5px dotted; */
+ background: #fff;
+}
+
+.array-form-element-decorator {
+ display: flex;
+ align-items: flex-start;
+}
+
+#root > .form-element:first-child {
+ border-left: none;
+ margin: 0;
+ padding: 0;
+}
+
+.shadow {
+ box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
+}
+
+.input-label {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.unknown-input {
+ color: #d84b4b;
+}
+
+.loader {
+ margin: auto;
+ width: 48px;
+ height: 48px;
+ border: 5px solid var(--bg-color);
+ border-bottom-color: transparent;
+ border-radius: 50%;
+ display: block;
+ box-sizing: border-box;
+ animation: rotation 1s linear infinite;
+}
+
+@keyframes rotation {
+ 0% {
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ transform: rotate(360deg);
+ }
+}