diff options
author | Bruce Berrios <[email protected]> | 2022-06-07 08:06:15 -0400 |
---|---|---|
committer | GitHub <[email protected]> | 2022-06-07 14:06:15 +0200 |
commit | f2998b1d28c9334ba50f9909db94bed363375e6f (patch) | |
tree | 3b806591f7c90c9002dea0e8618d50046f1499af /backend/private | |
parent | 5ed1f166dd56a8b022754d45bf41ce6487ea20fb (diff) | |
download | monkeytype-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.html | 60 | ||||
-rw-r--r-- | backend/private/script.js | 251 | ||||
-rw-r--r-- | backend/private/style.css | 187 |
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); + } +} |