diff --git a/.gitignore b/.gitignore index e1d1e52..9744485 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -/.vscode +*.vscode /dist /hetty /cmd/hetty/admin diff --git a/admin/.eslintrc.json b/admin/.eslintrc.json index e2acf76..a901afe 100644 --- a/admin/.eslintrc.json +++ b/admin/.eslintrc.json @@ -1,18 +1,51 @@ { - "extends": ["next/core-web-vitals", "prettier"], + "root": true, + "extends": ["next/core-web-vitals", "prettier", "plugin:@typescript-eslint/recommended", "plugin:import/typescript"], + "plugins": ["prettier", "@typescript-eslint", "import"], + "ignorePatterns": ["next*", "src/lib/graphql/generated.tsx"], + "settings": { + "import/parsers": { + "@typescript-eslint/parser": [".ts", ".tsx"] + }, + "import/resolver": { + "typescript": { + "alwaysTryTypes": true + } + } + }, "rules": { + "prettier/prettier": ["error"], "@next/next/no-css-tags": "off", - "@typescript-eslint/no-unused-vars": "off", - "unused-imports/no-unused-imports": "error", - "unused-imports/no-unused-vars": [ + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": "error", + + "import/default": "off", + + "import/no-unresolved": "error", + "import/named": "error", + "import/namespace": "error", + "import/export": "error", + "import/no-deprecated": "error", + "import/no-cycle": "error", + + "import/no-named-as-default": "warn", + "import/no-named-as-default-member": "warn", + "import/no-duplicates": "warn", + "import/newline-after-import": "warn", + "import/order": [ "warn", { - "vars": "all", - "varsIgnorePattern": "^_", - "args": "after-used", - "argsIgnorePattern": "^_" + "alphabetize": { "order": "asc", "caseInsensitive": false }, + "newlines-between": "always", + "groups": ["builtin", "external", "parent", "sibling", "index"] + } + ], + "import/no-unused-modules": [ + "error", + { + "missingExports": true, + "ignoreExports": ["./src/pages"] } ] - }, - "plugins": ["unused-imports", "prettier"] + } } diff --git a/admin/gqlcodegen.yml b/admin/gqlcodegen.yml index dbba8a0..5a9f02d 100644 --- a/admin/gqlcodegen.yml +++ b/admin/gqlcodegen.yml @@ -2,7 +2,7 @@ overwrite: true schema: "../pkg/api/schema.graphql" documents: "src/**/*.graphql" generates: - src/generated/graphql.tsx: + src/lib/graphql/generated.tsx: plugins: - "typescript" - "typescript-operations" diff --git a/admin/package.json b/admin/package.json index 4c95497..59c313d 100644 --- a/admin/package.json +++ b/admin/package.json @@ -44,8 +44,9 @@ "eslint": "^8.7.0", "eslint-config-next": "12.0.8", "eslint-config-prettier": "^8.3.0", + "eslint-import-resolver-typescript": "^2.5.0", + "eslint-plugin-import": "^2.25.4", "eslint-plugin-prettier": "^4.0.0", - "eslint-plugin-unused-imports": "^2.0.0", "prettier": "^2.1.2", "typescript": "^4.0.3", "webpack": "^5.67.0" diff --git a/admin/public/android-chrome-192x192.png b/admin/public/android-chrome-192x192.png new file mode 100644 index 0000000..5bc3e6d Binary files /dev/null and b/admin/public/android-chrome-192x192.png differ diff --git a/admin/public/android-chrome-512x512.png b/admin/public/android-chrome-512x512.png new file mode 100644 index 0000000..9ecc403 Binary files /dev/null and b/admin/public/android-chrome-512x512.png differ diff --git a/admin/public/apple-touch-icon.png b/admin/public/apple-touch-icon.png new file mode 100644 index 0000000..ee0d915 Binary files /dev/null and b/admin/public/apple-touch-icon.png differ diff --git a/admin/public/favicon-16x16.png b/admin/public/favicon-16x16.png new file mode 100644 index 0000000..61b96e5 Binary files /dev/null and b/admin/public/favicon-16x16.png differ diff --git a/admin/public/favicon-32x32.png b/admin/public/favicon-32x32.png new file mode 100644 index 0000000..54d2538 Binary files /dev/null and b/admin/public/favicon-32x32.png differ diff --git a/admin/public/favicon.ico b/admin/public/favicon.ico index 4965832..51fec0c 100644 Binary files a/admin/public/favicon.ico and b/admin/public/favicon.ico differ diff --git a/admin/public/site.webmanifest b/admin/public/site.webmanifest new file mode 100644 index 0000000..45dc8a2 --- /dev/null +++ b/admin/public/site.webmanifest @@ -0,0 +1 @@ +{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} \ No newline at end of file diff --git a/admin/src/components/projects/NewProject.tsx b/admin/src/components/projects/NewProject.tsx deleted file mode 100644 index f61c832..0000000 --- a/admin/src/components/projects/NewProject.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { gql, useMutation } from "@apollo/client"; -import { Box, Button, CircularProgress, TextField, Typography } from "@mui/material"; -import AddIcon from "@mui/icons-material/Add"; -import React, { useState } from "react"; - -const CREATE_PROJECT = gql` - mutation CreateProject($name: String!) { - createProject(name: $name) { - id - name - } - } -`; - -const OPEN_PROJECT = gql` - mutation OpenProject($id: ID!) { - openProject(id: $id) { - id - name - isActive - } - } -`; - -function NewProject(): JSX.Element { - const [name, setName] = useState(""); - - const [createProject, { error: createProjErr, loading: createProjLoading }] = useMutation(CREATE_PROJECT, { - onError: () => {}, - onCompleted(data) { - setName(""); - openProject({ variables: { id: data.createProject.id } }); - }, - }); - const [openProject, { error: openProjErr, loading: openProjLoading }] = useMutation(OPEN_PROJECT, { - onError: () => {}, - update(cache, { data: { openProject } }) { - cache.modify({ - fields: { - activeProject() { - const activeProjRef = cache.writeFragment({ - id: openProject.id, - data: openProject, - fragment: gql` - fragment ActiveProject on Project { - id - name - isActive - type - } - `, - }); - return activeProjRef; - }, - projects(_, { DELETE }) { - cache.writeFragment({ - id: openProject.id, - data: openProject, - fragment: gql` - fragment OpenProject on Project { - id - name - isActive - type - } - `, - }); - return DELETE; - }, - }, - }); - }, - }); - - const handleCreateAndOpenProjectForm = (e: React.SyntheticEvent) => { - e.preventDefault(); - createProject({ variables: { name } }); - }; - - return ( -
- - New project - -
- setName(e.target.value)} - error={Boolean(createProjErr || openProjErr)} - helperText={(createProjErr && createProjErr.message) || (openProjErr && openProjErr.message)} - /> - - -
- ); -} - -export default NewProject; diff --git a/admin/src/components/reqlog/hooks/useClearHTTPRequestLog.ts b/admin/src/components/reqlog/hooks/useClearHTTPRequestLog.ts deleted file mode 100644 index 2870712..0000000 --- a/admin/src/components/reqlog/hooks/useClearHTTPRequestLog.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { gql, useMutation } from "@apollo/client"; -import { HTTP_REQUEST_LOGS } from "./useHttpRequestLogs"; - -const CLEAR_HTTP_REQUEST_LOG = gql` - mutation ClearHTTPRequestLog { - clearHTTPRequestLog { - success - } - } -`; - -export function useClearHTTPRequestLog() { - return useMutation(CLEAR_HTTP_REQUEST_LOG, { - refetchQueries: [{ query: HTTP_REQUEST_LOGS }], - }); -} diff --git a/admin/src/components/reqlog/hooks/useCreateSenderRequest.ts b/admin/src/components/reqlog/hooks/useCreateSenderRequest.ts deleted file mode 100644 index aa81efe..0000000 --- a/admin/src/components/reqlog/hooks/useCreateSenderRequest.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { gql, useMutation } from "@apollo/client"; - -const CREATE_SENDER_REQUEST = gql` - mutation CreateSenderRequest($request: SenderRequestInput!) { - createSenderRequest(request: $request) { - id - } - } -`; - -export default function useCreateSenderRequest() { - return useMutation(CREATE_SENDER_REQUEST); -} diff --git a/admin/src/components/reqlog/hooks/useHttpRequestLogs.ts b/admin/src/components/reqlog/hooks/useHttpRequestLogs.ts deleted file mode 100644 index f426702..0000000 --- a/admin/src/components/reqlog/hooks/useHttpRequestLogs.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { gql, useQuery } from "@apollo/client"; - -export const HTTP_REQUEST_LOGS = gql` - query HttpRequestLogs { - httpRequestLogs { - id - method - url - timestamp - response { - statusCode - statusReason - } - } - } -`; - -export function useHttpRequestLogs() { - return useQuery(HTTP_REQUEST_LOGS, { - pollInterval: 1000, - }); -} diff --git a/admin/src/components/Layout.tsx b/admin/src/features/Layout.tsx similarity index 98% rename from admin/src/components/Layout.tsx rename to admin/src/features/Layout.tsx index 0c6302c..5368793 100644 --- a/admin/src/components/Layout.tsx +++ b/admin/src/features/Layout.tsx @@ -1,4 +1,11 @@ -import React from "react"; +import ChevronLeftIcon from "@mui/icons-material/ChevronLeft"; +import ChevronRightIcon from "@mui/icons-material/ChevronRight"; +import FolderIcon from "@mui/icons-material/Folder"; +import HomeIcon from "@mui/icons-material/Home"; +import LocationSearchingIcon from "@mui/icons-material/LocationSearching"; +import MenuIcon from "@mui/icons-material/Menu"; +import SendIcon from "@mui/icons-material/Send"; +import SettingsEthernetIcon from "@mui/icons-material/SettingsEthernet"; import { Theme, useTheme, @@ -18,14 +25,7 @@ import MuiDrawer from "@mui/material/Drawer"; import MuiListItemButton, { ListItemButtonProps } from "@mui/material/ListItemButton"; import MuiListItemIcon, { ListItemIconProps } from "@mui/material/ListItemIcon"; import Link from "next/link"; -import MenuIcon from "@mui/icons-material/Menu"; -import HomeIcon from "@mui/icons-material/Home"; -import SettingsEthernetIcon from "@mui/icons-material/SettingsEthernet"; -import SendIcon from "@mui/icons-material/Send"; -import FolderIcon from "@mui/icons-material/Folder"; -import LocationSearchingIcon from "@mui/icons-material/LocationSearching"; -import ChevronLeftIcon from "@mui/icons-material/ChevronLeft"; -import ChevronRightIcon from "@mui/icons-material/ChevronRight"; +import React, { useState } from "react"; export enum Page { Home, @@ -133,7 +133,7 @@ interface Props { export function Layout({ title, page, children }: Props): JSX.Element { const theme = useTheme(); - const [open, setOpen] = React.useState(false); + const [open, setOpen] = useState(false); const handleDrawerOpen = () => { setOpen(true); @@ -247,5 +247,3 @@ export function Layout({ title, page, children }: Props): JSX.Element { ); } - -export default Layout; diff --git a/admin/src/features/projects/components/NewProject.tsx b/admin/src/features/projects/components/NewProject.tsx new file mode 100644 index 0000000..550b04e --- /dev/null +++ b/admin/src/features/projects/components/NewProject.tsx @@ -0,0 +1,67 @@ +import AddIcon from "@mui/icons-material/Add"; +import { Box, Button, CircularProgress, TextField, Typography } from "@mui/material"; +import React, { useState } from "react"; + +import useOpenProjectMutation from "../hooks/useOpenProjectMutation"; + +import { useCreateProjectMutation } from "lib/graphql/generated"; + +function NewProject(): JSX.Element { + const [name, setName] = useState(""); + + const [createProject, createProjResult] = useCreateProjectMutation({ + onCompleted(data) { + setName(""); + if (data?.createProject) { + openProject({ variables: { id: data.createProject?.id } }); + } + }, + }); + const [openProject, openProjResult] = useOpenProjectMutation(); + + const handleCreateAndOpenProjectForm = (e: React.SyntheticEvent) => { + e.preventDefault(); + createProject({ variables: { name } }); + }; + + return ( +
+ + New project + +
+ setName(e.target.value)} + error={Boolean(createProjResult.error || openProjResult.error)} + helperText={ + (createProjResult.error && createProjResult.error.message) || + (openProjResult.error && openProjResult.error.message) + } + /> + + +
+ ); +} + +export default NewProject; diff --git a/admin/src/components/projects/ProjectList.tsx b/admin/src/features/projects/components/ProjectList.tsx similarity index 63% rename from admin/src/components/projects/ProjectList.tsx rename to admin/src/features/projects/components/ProjectList.tsx index e3a6987..3ad2dd0 100644 --- a/admin/src/components/projects/ProjectList.tsx +++ b/admin/src/features/projects/components/ProjectList.tsx @@ -1,4 +1,8 @@ -import { gql, useMutation, useQuery } from "@apollo/client"; +import CloseIcon from "@mui/icons-material/Close"; +import DeleteIcon from "@mui/icons-material/Delete"; +import DescriptionIcon from "@mui/icons-material/Description"; +import LaunchIcon from "@mui/icons-material/Launch"; +import { Alert } from "@mui/lab"; import { Avatar, Box, @@ -21,110 +25,25 @@ import { Typography, useTheme, } from "@mui/material"; -import CloseIcon from "@mui/icons-material/Close"; -import DescriptionIcon from "@mui/icons-material/Description"; -import DeleteIcon from "@mui/icons-material/Delete"; -import LaunchIcon from "@mui/icons-material/Launch"; -import { Alert } from "@mui/lab"; import React, { useState } from "react"; -import { Project } from "../../lib/Project"; +import useOpenProjectMutation from "../hooks/useOpenProjectMutation"; -const PROJECTS = gql` - query Projects { - projects { - id - name - isActive - } - } -`; - -const OPEN_PROJECT = gql` - mutation OpenProject($id: ID!) { - openProject(id: $id) { - id - name - isActive - } - } -`; - -const CLOSE_PROJECT = gql` - mutation CloseProject { - closeProject { - success - } - } -`; - -const DELETE_PROJECT = gql` - mutation DeleteProject($id: ID!) { - deleteProject(id: $id) { - success - } - } -`; +import { + ProjectsQuery, + useCloseProjectMutation, + useDeleteProjectMutation, + useProjectsQuery, +} from "lib/graphql/generated"; function ProjectList(): JSX.Element { const theme = useTheme(); - const { - loading: projLoading, - error: projErr, - data: projData, - } = useQuery<{ projects: Project[] }>(PROJECTS, { - fetchPolicy: "network-only", - }); - const [openProject, { error: openProjErr, loading: openProjLoading }] = useMutation<{ openProject: Project }>( - OPEN_PROJECT, - { - errorPolicy: "all", - onError: () => {}, - update(cache, { data }) { - cache.modify({ - fields: { - activeProject() { - const activeProjRef = cache.writeFragment({ - data: data?.openProject, - fragment: gql` - fragment ActiveProject on Project { - id - name - isActive - type - } - `, - }); - return activeProjRef; - }, - projects(_, { DELETE }) { - cache.writeFragment({ - id: data?.openProject.id, - data: openProject, - fragment: gql` - fragment OpenProject on Project { - id - name - isActive - type - } - `, - }); - return DELETE; - }, - httpRequestLogFilter(_, { DELETE }) { - return DELETE; - }, - }, - }); - }, - } - ); - const [closeProject, { error: closeProjErr, client }] = useMutation(CLOSE_PROJECT, { + const projResult = useProjectsQuery({ fetchPolicy: "network-only" }); + const [openProject, openProjResult] = useOpenProjectMutation(); + const [closeProject, closeProjResult] = useCloseProjectMutation({ errorPolicy: "all", - onError: () => {}, onCompleted() { - client.resetStore(); + closeProjResult.client.resetStore(); }, update(cache) { cache.modify({ @@ -142,9 +61,8 @@ function ProjectList(): JSX.Element { }); }, }); - const [deleteProject, { loading: deleteProjLoading, error: deleteProjErr }] = useMutation(DELETE_PROJECT, { + const [deleteProject, deleteProjResult] = useDeleteProjectMutation({ errorPolicy: "all", - onError: () => {}, update(cache) { cache.modify({ fields: { @@ -158,14 +76,16 @@ function ProjectList(): JSX.Element { }, }); - const [deleteProj, setDeleteProj] = useState(); + const [deleteProj, setDeleteProj] = useState(); const [deleteDiagOpen, setDeleteDiagOpen] = useState(false); - const handleDeleteButtonClick = (project: any) => { + const handleDeleteButtonClick = (project: ProjectsQuery["projects"][number]) => { setDeleteProj(project); setDeleteDiagOpen(true); }; const handleDeleteConfirm = () => { - deleteProject({ variables: { id: deleteProj?.id } }); + if (deleteProj) { + deleteProject({ variables: { id: deleteProj.id } }); + } }; const handleDeleteCancel = () => { setDeleteDiagOpen(false); @@ -189,7 +109,9 @@ function ProjectList(): JSX.Element { Deleting a project permanently removes all its data from the database. This action is irreversible. - {deleteProjErr && Error closing project: {deleteProjErr.message}} + {deleteProjResult.error && ( + Error closing project: {deleteProjResult.error.message} + )}