From 938328cd077d40b75e0834d56ff8d43ad035fd2b Mon Sep 17 00:00:00 2001 From: f0x52 Date: Thu, 29 Sep 2022 12:02:41 +0200 Subject: [frontend] Unified panels (#812) * settings panel restructuring * clean up old Gin handlers * colorscheme redesign, some other small css tweaks * basic router layout, error boundary * colorscheme redesign, some other small css tweaks * kebab-case consistency * superfluous padding on applist * remove unused consts * redux, whitespace changes.. * use .jsx extensions for components * login flow up till app registration * full redux oauth implementation, with basic error handling * split oauth api functions * oauth api revocation handling * basic profile change submission * move old dir * profile overview * fix keeping track of the wrong instance url (for different instance/api domains) * use redux state for profile form * delete old/index.js, old/basic.js, fully implemented * implement old/user/profile.js * implement password change * remove debug logging * support future api for removing files * customize profile css * remove unneeded wrapper components * restructure form fields * start on admin pages * admin panel settings * admin settings panel * remove old/admin files * add top-level redirect * refactor/cleanup forms * only do API checks on logged-in state * admin-status based routing * federation block routing * federation blocks * upgrade dependencies * react 18 changes * media cleanup * fix useEffect hooks * remove unused require * custom emoji base * emoji uploader * delete last old panel files * sidebar styling, remove unused page * refactor submit functions * fix sidebar boxshadow-border * fix old css variables * fix fake-toot avatar * fix non-square emoji * fix user settings redux keys * properly get admin account contact from instance response * Account.source default values * source.status_format key * mobile responsiveness * mobile element tweaks * proper redirect after removing block * add redirects for old setting panel urls * deletes * fix mobile overflow * clean up debug logging calls --- web/source/panels/admin/README.md | 21 --- web/source/panels/admin/blocks.js | 318 ------------------------------------ web/source/panels/admin/index.js | 64 -------- web/source/panels/admin/settings.js | 182 --------------------- web/source/panels/admin/style.css | 106 ------------ 5 files changed, 691 deletions(-) delete mode 100644 web/source/panels/admin/README.md delete mode 100644 web/source/panels/admin/blocks.js delete mode 100644 web/source/panels/admin/index.js delete mode 100644 web/source/panels/admin/settings.js delete mode 100644 web/source/panels/admin/style.css (limited to 'web/source/panels/admin') diff --git a/web/source/panels/admin/README.md b/web/source/panels/admin/README.md deleted file mode 100644 index 9a4572270..000000000 --- a/web/source/panels/admin/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# GoToSocial Admin Panel - -Standalone web admin panel for [GoToSocial](https://github.com/superseriousbusiness/gotosocial). - -A public hosted instance is also available at https://gts.superseriousbusiness.org/admin/, so you can fill your own instance URL in there. - -## Installation -Build requirements: some version of Node.js with npm, -``` -git clone https://github.com/superseriousbusiness/gotosocial-admin.git && cd gotosocial-admin -npm install -node index.js -``` -All processed build output will now be in `public/`, which you can copy over to a folder in your GoToSocial installation like `web/assets/admin`, or serve elsewhere. -No further configuration is required, authentication happens through normal OAUTH flow. - -## Development -Follow the installation steps, but run `NODE_ENV=development node index.js` to start the livereloading dev server instead. - -## License, donations -[AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.html). If you want to support my work, you can: Donate using Liberapay \ No newline at end of file diff --git a/web/source/panels/admin/blocks.js b/web/source/panels/admin/blocks.js deleted file mode 100644 index b12eb50a9..000000000 --- a/web/source/panels/admin/blocks.js +++ /dev/null @@ -1,318 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . -*/ - -"use strict"; - -const Promise = require("bluebird"); -const React = require("react"); -const fileDownload = require("js-file-download"); - -function sortBlocks(blocks) { - return blocks.sort((a, b) => { // alphabetical sort - return a.domain.localeCompare(b.domain); - }); -} - -function deduplicateBlocks(blocks) { - let a = new Map(); - blocks.forEach((block) => { - a.set(block.id, block); - }); - return Array.from(a.values()); -} - -module.exports = function Blocks({oauth}) { - const [blocks, setBlocks] = React.useState([]); - const [info, setInfo] = React.useState("Fetching blocks"); - const [errorMsg, setError] = React.useState(""); - const [checked, setChecked] = React.useState(new Set()); - - React.useEffect(() => { - Promise.try(() => { - return oauth.apiRequest("/api/v1/admin/domain_blocks", undefined, undefined, "GET"); - }).then((json) => { - setInfo(""); - setError(""); - setBlocks(sortBlocks(json)); - }).catch((e) => { - setError(e.message); - setInfo(""); - }); - }, []); - - let blockList = blocks.map((block) => { - function update(e) { - let newChecked = new Set(checked.values()); - if (e.target.checked) { - newChecked.add(block.id); - } else { - newChecked.delete(block.id); - } - setChecked(newChecked); - } - - return ( - -
-
{block.domain}
-
{(new Date(block.created_at)).toLocaleString()}
-
- ); - }); - - function clearChecked() { - setChecked(new Set()); - } - - function undoChecked() { - let amount = checked.size; - if(confirm(`Are you sure you want to remove ${amount} block(s)?`)) { - setInfo(""); - Promise.map(Array.from(checked.values()), (block) => { - console.log("deleting", block); - return oauth.apiRequest(`/api/v1/admin/domain_blocks/${block}`, "DELETE"); - }).then((res) => { - console.log(res); - setInfo(`Deleted ${amount} blocks: ${res.map((a) => a.domain).join(", ")}`); - }).catch((e) => { - setError(e); - }); - - let newBlocks = blocks.filter((block) => { - if (checked.size > 0 && checked.has(block.id)) { - checked.delete(block.id); - return false; - } else { - return true; - } - }); - setBlocks(newBlocks); - clearChecked(); - } - } - - return ( -
-

Blocks

-
{errorMsg}
-
{info}
- -

Blocks:

-
- uncheck all - -
-
- {blockList} -
- -
- ); -}; - -function BulkBlocking({oauth, blocks, setBlocks}) { - const [bulk, setBulk] = React.useState(""); - const [blockMap, setBlockMap] = React.useState(new Map()); - const [output, setOutput] = React.useState(); - - React.useEffect(() => { - let newBlockMap = new Map(); - blocks.forEach((block) => { - newBlockMap.set(block.domain, block); - }); - setBlockMap(newBlockMap); - }, [blocks]); - - const fileRef = React.useRef(); - - function error(e) { - setOutput(
{e}
); - throw e; - } - - function fileUpload() { - let reader = new FileReader(); - reader.addEventListener("load", (e) => { - try { - // TODO: use validatem? - let json = JSON.parse(e.target.result); - json.forEach((block) => { - console.log("block:", block); - }); - } catch(e) { - error(e.message); - } - }); - reader.readAsText(fileRef.current.files[0]); - } - - React.useEffect(() => { - if (fileRef && fileRef.current) { - fileRef.current.addEventListener("change", fileUpload); - } - return function cleanup() { - fileRef.current.removeEventListener("change", fileUpload); - }; - }); - - function textImport() { - Promise.try(() => { - if (bulk[0] == "[") { - // assume it's json - return JSON.parse(bulk); - } else { - return bulk.split("\n").map((val) => { - return { - domain: val.trim() - }; - }); - } - }).then((domains) => { - console.log(domains); - let before = domains.length; - setOutput(`Importing ${before} domain(s)`); - domains = domains.filter(({domain}) => { - return (domain != "" && !blockMap.has(domain)); - }); - setOutput({output}
{`Deduplicated ${before - domains.length}/${before} with existing blocks, adding ${domains.length} block(s)`}
); - if (domains.length > 0) { - let data = new FormData(); - data.append("domains", new Blob([JSON.stringify(domains)], {type: "application/json"}), "import.json"); - return oauth.apiRequest("/api/v1/admin/domain_blocks?import=true", "POST", data, "form"); - } - }).then((json) => { - console.log("bulk import result:", json); - setBlocks(sortBlocks(deduplicateBlocks([...json, ...blocks]))); - }).catch((e) => { - error(e.message); - }); - } - - function textExport() { - setBulk(blocks.reduce((str, val) => { - if (typeof str == "object") { - return str.domain; - } else { - return str + "\n" + val.domain; - } - })); - } - - function jsonExport() { - Promise.try(() => { - return oauth.apiRequest("/api/v1/admin/domain_blocks?export=true", "GET"); - }).then((json) => { - fileDownload(JSON.stringify(json), "block-export.json"); - }).catch((e) => { - error(e); - }); - } - - function textAreaUpdate(e) { - setBulk(e.target.value); - } - - return ( - -

Bulk import/export

- - -
- - - - -
- {output} - -
- ); -} - -function AddBlock({oauth, blocks, setBlocks}) { - const [domain, setDomain] = React.useState(""); - const [type, setType] = React.useState("suspend"); - const [obfuscated, setObfuscated] = React.useState(false); - const [privateDescription, setPrivateDescription] = React.useState(""); - const [publicDescription, setPublicDescription] = React.useState(""); - - function addBlock() { - console.log(`${type}ing`, domain); - Promise.try(() => { - return oauth.apiRequest("/api/v1/admin/domain_blocks", "POST", { - domain: domain, - obfuscate: obfuscated, - private_comment: privateDescription, - public_comment: publicDescription - }, "json"); - }).then((json) => { - setDomain(""); - setPrivateDescription(""); - setPublicDescription(""); - setBlocks([json, ...blocks]); - }); - } - - function onDomainChange(e) { - setDomain(e.target.value); - } - - function onTypeChange(e) { - setType(e.target.value); - } - - function onKeyDown(e) { - if (e.key == "Enter") { - addBlock(); - } - } - - return ( - -

Add Block:

-
- - - -
-
- -
-
-
- -
-
- - setObfuscated(e.target.checked)}/> -
-
-
- ); -} - -// function Blocklist() { -// return ( -//
-//

Blocklists

-//
-// ); -// } \ No newline at end of file diff --git a/web/source/panels/admin/index.js b/web/source/panels/admin/index.js deleted file mode 100644 index 0fc1601eb..000000000 --- a/web/source/panels/admin/index.js +++ /dev/null @@ -1,64 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . -*/ - -"use strict"; - -const Promise = require("bluebird"); -const React = require("react"); -const ReactDom = require("react-dom"); - -const createPanel = require("../lib/panel"); - -const Settings = require("./settings"); -const Blocks = require("./blocks"); - -require("../base.css"); -require("./style.css"); - -function AdminPanel({oauth}) { - /* - Features: (issue #78) - - [ ] Instance information updating - GET /api/v1/instance PATCH /api/v1/instance - - [ ] Domain block creation, viewing, and deletion - GET /api/v1/admin/domain_blocks - POST /api/v1/admin/domain_blocks - GET /api/v1/admin/domain_blocks/DOMAIN_BLOCK_ID, DELETE /api/v1/admin/domain_blocks/DOMAIN_BLOCK_ID - - [ ] Blocklist import/export - GET /api/v1/admin/domain_blocks?export=true - POST json file as form field domains to /api/v1/admin/domain_blocks - */ - - return ( - - - - - - ); -} - -function Logout({oauth}) { - return ( -
- -
- ); -} - -createPanel("GoToSocial Admin Panel", ["admin"], AdminPanel); \ No newline at end of file diff --git a/web/source/panels/admin/settings.js b/web/source/panels/admin/settings.js deleted file mode 100644 index c9f470464..000000000 --- a/web/source/panels/admin/settings.js +++ /dev/null @@ -1,182 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . -*/ - -"use strict"; - -const Promise = require("bluebird"); -const React = require("react"); - -module.exports = function Settings({oauth}) { - const [info, setInfo] = React.useState({}); - const [errorMsg, setError] = React.useState(""); - const [statusMsg, setStatus] = React.useState("Fetching instance info"); - - React.useEffect(() => { - Promise.try(() => { - return oauth.apiRequest("/api/v1/instance", "GET"); - }).then((json) => { - setInfo(json); - }).catch((e) => { - setError(e.message); - setStatus(""); - }); - }, []); - - function submit() { - setStatus("PATCHing"); - setError(""); - return Promise.try(() => { - let formDataInfo = new FormData(); - Object.entries(info).forEach(([key, val]) => { - if (key == "contact_account") { - key = "contact_username"; - val = val.username; - } - if (key == "email") { - key = "contact_email"; - } - if (typeof val != "object") { - formDataInfo.append(key, val); - } - }); - return oauth.apiRequest("/api/v1/instance", "PATCH", formDataInfo, "form"); - }).then((json) => { - setStatus("Config saved"); - console.log(json); - }).catch((e) => { - setError(e.message); - setStatus(""); - }); - } - - return ( -
-

Instance Information

-
- {errorMsg} -
-
- {statusMsg} -
-
e.preventDefault()}> - {editableObject(info)} -
-
- ); -}; - -function editableObject(obj, path=[]) { - const readOnlyKeys = ["uri", "version", "urls_streaming_api", "stats"]; - const hiddenKeys = ["contact_account_", "urls"]; - const explicitShownKeys = ["contact_account_username"]; - const implementedKeys = "title, contact_account_username, email, short_description, description, terms, avatar, header".split(", "); - const textareaKeys = ["short_description", "description"] - - let listing = Object.entries(obj).map(([key, val]) => { - let fullkey = [...path, key].join("_"); - - if ( - hiddenKeys.includes(fullkey) || - hiddenKeys.includes(path.join("_")+"_") // also match just parent path - ) { - if (!explicitShownKeys.includes(fullkey)) { - return null; - } - } - - if (Array.isArray(val)) { - // FIXME: handle this - } else if (typeof val == "object") { - return ( - {editableObject(val, [...path, key])} - ); - } - - let isImplemented = ""; - if (!implementedKeys.includes(fullkey)) { - isImplemented = " notImplemented"; - } - - let isReadOnly = ( - readOnlyKeys.includes(fullkey) || - readOnlyKeys.includes(path.join("_")) || - isImplemented != "" - ); - - let label = key.replace(/_/g, " "); - if (path.length > 0) { - label = `\u00A0`.repeat(4 * path.length) + label; - } - - let inputProps; - let changeFunc; - if (val === true || val === false) { - inputProps = { - type: "checkbox", - defaultChecked: val, - disabled: isReadOnly - }; - changeFunc = (e) => e.target.checked; - } else if (val.length != 0 && !isNaN(val)) { - inputProps = { - type: "number", - defaultValue: val, - readOnly: isReadOnly - }; - changeFunc = (e) => e.target.value; - } else { - inputProps = { - type: "text", - defaultValue: val, - readOnly: isReadOnly - }; - changeFunc = (e) => e.target.value; - } - - function setRef(element) { - if (element != null) { - element.addEventListener("change", (e) => { - obj[key] = changeFunc(e); - }); - } - } - - let field; - if (textareaKeys.includes(fullkey)) { - field = - } else { - field = - } - return ( - - -
- {field} -
-
- ); - }); - return ( - - {path != "" && - <>{path}: - } - {listing} - - ); -} \ No newline at end of file diff --git a/web/source/panels/admin/style.css b/web/source/panels/admin/style.css deleted file mode 100644 index 01195437f..000000000 --- a/web/source/panels/admin/style.css +++ /dev/null @@ -1,106 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . -*/ - -section.info { - form { - grid-template-columns: auto 1fr; - width: calc(100% - 0.35rem); - - input { - width: 100%; - line-height: 1.5rem; - } - - label, input { - padding: 0.2rem 0.5rem; - } - - input[type=checkbox] { - justify-self: start; - width: initial; - } - - input:read-only { - border: none; - } - - input:invalid { - border-color: red; - } - } - - textarea { - width: 100%; - height: 8rem; - } - - h1 { - display: flex; - justify-content: space-between; - margin-bottom: 0.5rem; - } -} - -section.blocks { - .overflow { - max-height: 80vh; - overflow-y: auto; - } - - .blocklist { - display: grid; - grid-template-columns: auto 1fr auto; - grid-gap: 0.35rem 0; - - div { - background: rgb(70, 79, 88); - padding: 0.2rem 0.4rem; - } - } - - .addblock { - display: grid; - grid-template-columns: 1fr auto auto; - grid-gap: 0.35rem; - - input, select { - font-size: 1.2rem; - } - - input, select, textarea { - padding: 0.5rem; - } - - div { - grid-column: 1/4; - } - - div.single input { - width: initial; - } - } - - h3 { - margin-bottom: 0; - } - - .controls { - display: flex; - gap: 0.5rem; - } -} -- cgit v1.2.3