ui(admin): update button components, fix Editor for eggs

This commit is contained in:
Matthew Penner 2022-12-21 14:27:50 -07:00
parent 4e56f6dbea
commit 089860b721
No known key found for this signature in database
35 changed files with 363 additions and 623 deletions

View file

@ -85,7 +85,7 @@ export const searchEggs = async (
return data.data.map(Transformers.toEgg); return data.data.map(Transformers.toEgg);
}; };
export const exportEgg = async (eggId: number): Promise<Record<string, any>> => { export const exportEgg = async (eggId: number): Promise<string> => {
const { data } = await http.get(`/api/application/eggs/${eggId}/export`); const { data } = await http.get(`/api/application/eggs/${eggId}/export`);
return data; return data;
}; };

View file

@ -1,13 +1,11 @@
import http, { import type { AxiosError } from 'axios';
FractalPaginatedResponse, import type { SWRConfiguration, SWRResponse } from 'swr';
PaginatedResult, import useSWR from 'swr';
QueryBuilderParams,
getPaginationSet, import type { FractalPaginatedResponse, PaginatedResult, QueryBuilderParams } from '@/api/http';
withQueryBuilderParams, import http, { getPaginationSet, withQueryBuilderParams } from '@/api/http';
} from '@/api/http'; import type { User } from '@definitions/admin';
import { Transformers, User } from '@definitions/admin'; import { Transformers } from '@definitions/admin';
import useSWR, { SWRConfiguration, SWRResponse } from 'swr';
import { AxiosError } from 'axios';
export interface UpdateUserValues { export interface UpdateUserValues {
externalId: string; externalId: string;
@ -32,7 +30,10 @@ const useGetUsers = (
params: withQueryBuilderParams(params), params: withQueryBuilderParams(params),
}); });
return getPaginationSet(data, Transformers.toUser); return {
items: (data.data || []).map(Transformers.toUser),
pagination: getPaginationSet(data.meta.pagination),
};
}, },
config || { revalidateOnMount: true, revalidateOnFocus: false }, config || { revalidateOnMount: true, revalidateOnFocus: false },
); );

View file

@ -28,6 +28,8 @@ interface ExtendedWindow extends Window {
root_admin: boolean; root_admin: boolean;
use_totp: boolean; use_totp: boolean;
language: string; language: string;
avatar_url: string;
admin_role_name: string;
updated_at: string; updated_at: string;
created_at: string; created_at: string;
/* eslint-enable camelcase */ /* eslint-enable camelcase */
@ -45,6 +47,8 @@ function App() {
email: PterodactylUser.email, email: PterodactylUser.email,
language: PterodactylUser.language, language: PterodactylUser.language,
rootAdmin: PterodactylUser.root_admin, rootAdmin: PterodactylUser.root_admin,
avatarURL: PterodactylUser.avatar_url,
roleName: PterodactylUser.admin_role_name,
useTotp: PterodactylUser.use_totp, useTotp: PterodactylUser.use_totp,
createdAt: new Date(PterodactylUser.created_at), createdAt: new Date(PterodactylUser.created_at),
updatedAt: new Date(PterodactylUser.updated_at), updatedAt: new Date(PterodactylUser.updated_at),

View file

@ -1,7 +1,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { Link, NavLink } from 'react-router-dom'; import { Link, NavLink } from 'react-router-dom';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCogs, faLayerGroup, faSignOutAlt } from '@fortawesome/free-solid-svg-icons'; import { faLayerGroup, faScrewdriverWrench, faSignOutAlt } from '@fortawesome/free-solid-svg-icons';
import { useStoreState } from 'easy-peasy'; import { useStoreState } from 'easy-peasy';
import { ApplicationStore } from '@/state'; import { ApplicationStore } from '@/state';
import SearchContainer from '@/components/dashboard/search/SearchContainer'; import SearchContainer from '@/components/dashboard/search/SearchContainer';
@ -57,6 +57,7 @@ export default () => {
{name} {name}
</Link> </Link>
</div> </div>
<RightNavigation className="flex h-full items-center justify-center"> <RightNavigation className="flex h-full items-center justify-center">
<SearchContainer /> <SearchContainer />
@ -66,14 +67,6 @@ export default () => {
</NavLink> </NavLink>
</Tooltip> </Tooltip>
{rootAdmin && (
<Tooltip placement="bottom" content="Admin">
<a href="/admin" rel="noreferrer">
<FontAwesomeIcon icon={faCogs} />
</a>
</Tooltip>
)}
<Tooltip placement="bottom" content="Account Settings"> <Tooltip placement="bottom" content="Account Settings">
<NavLink to="/account"> <NavLink to="/account">
<span className="flex items-center w-5 h-5"> <span className="flex items-center w-5 h-5">
@ -82,6 +75,14 @@ export default () => {
</NavLink> </NavLink>
</Tooltip> </Tooltip>
{rootAdmin && (
<Tooltip placement="bottom" content="Admin">
<a href="/admin" rel="noreferrer">
<FontAwesomeIcon icon={faScrewdriverWrench} />
</a>
</Tooltip>
)}
<Tooltip placement="bottom" content="Sign Out"> <Tooltip placement="bottom" content="Sign Out">
<button onClick={onTriggerLogout}> <button onClick={onTriggerLogout}>
<FontAwesomeIcon icon={faSignOutAlt} /> <FontAwesomeIcon icon={faSignOutAlt} />

View file

@ -35,7 +35,7 @@ interface PropsWithoutIcon extends Props {
} }
export const SubNavigationLink = ({ to, name, icon: IconComponent, children }: PropsWithIcon | PropsWithoutIcon) => ( export const SubNavigationLink = ({ to, name, icon: IconComponent, children }: PropsWithIcon | PropsWithoutIcon) => (
<NavLink to={to}> <NavLink to={to} end>
{IconComponent ? <IconComponent /> : children} {IconComponent ? <IconComponent /> : children}
{name} {name}
</NavLink> </NavLink>

View file

@ -1,9 +1,11 @@
import { Actions, useStoreActions } from 'easy-peasy'; import type { Actions } from 'easy-peasy';
import { useStoreActions } from 'easy-peasy';
import { useState } from 'react'; import { useState } from 'react';
import tw from 'twin.macro'; import tw from 'twin.macro';
import deleteDatabase from '@/api/admin/databases/deleteDatabase'; import deleteDatabase from '@/api/admin/databases/deleteDatabase';
import Button from '@/components/elements/Button'; import { Button } from '@/components/elements/button';
import { Shape } from '@/components/elements/button/types';
import ConfirmationModal from '@/components/elements/ConfirmationModal'; import ConfirmationModal from '@/components/elements/ConfirmationModal';
import type { ApplicationStore } from '@/state'; import type { ApplicationStore } from '@/state';
@ -52,7 +54,7 @@ export default ({ databaseId, onDeleted }: Props) => {
created on this host but not the databases themselves. created on this host but not the databases themselves.
</ConfirmationModal> </ConfirmationModal>
<Button type={'button'} size={'xsmall'} color={'red'} onClick={() => setVisible(true)}> <Button.Danger type="button" shape={Shape.IconSquare} onClick={() => setVisible(true)}>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
@ -67,7 +69,7 @@ export default ({ databaseId, onDeleted }: Props) => {
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/> />
</svg> </svg>
</Button> </Button.Danger>
</> </>
); );
}; };

View file

@ -14,7 +14,7 @@ import AdminContentBlock from '@/components/admin/AdminContentBlock';
import Spinner from '@/components/elements/Spinner'; import Spinner from '@/components/elements/Spinner';
import FlashMessageRender from '@/components/FlashMessageRender'; import FlashMessageRender from '@/components/FlashMessageRender';
import AdminBox from '@/components/admin/AdminBox'; import AdminBox from '@/components/admin/AdminBox';
import Button from '@/components/elements/Button'; import { Button } from '@/components/elements/button';
import Field from '@/components/elements/Field'; import Field from '@/components/elements/Field';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
import DatabaseDeleteButton from '@/components/admin/databases/DatabaseDeleteButton'; import DatabaseDeleteButton from '@/components/admin/databases/DatabaseDeleteButton';
@ -115,7 +115,7 @@ export const InformationContainer = ({ title, initialValues, children, onSubmit
<div css={tw`w-full flex flex-row items-center mt-6`}> <div css={tw`w-full flex flex-row items-center mt-6`}>
{children} {children}
<div css={tw`flex ml-auto`}> <div css={tw`flex ml-auto`}>
<Button type={'submit'} disabled={isSubmitting || !isValid}> <Button type="submit" disabled={isSubmitting || !isValid}>
Save Changes Save Changes
</Button> </Button>
</div> </div>

View file

@ -10,17 +10,18 @@ import FlashMessageRender from '@/components/FlashMessageRender';
import AdminCheckbox from '@/components/admin/AdminCheckbox'; import AdminCheckbox from '@/components/admin/AdminCheckbox';
import AdminContentBlock from '@/components/admin/AdminContentBlock'; import AdminContentBlock from '@/components/admin/AdminContentBlock';
import AdminTable, { import AdminTable, {
ContentWrapper,
Loading,
NoItems,
Pagination,
TableBody, TableBody,
TableHead, TableHead,
TableHeader, TableHeader,
TableRow, TableRow,
Pagination,
Loading,
NoItems,
ContentWrapper,
useTableHooks, useTableHooks,
} from '@/components/admin/AdminTable'; } from '@/components/admin/AdminTable';
import Button from '@/components/elements/Button'; import { Button } from '@/components/elements/button';
import { Size } from '@/components/elements/button/types';
import CopyOnClick from '@/components/elements/CopyOnClick'; import CopyOnClick from '@/components/elements/CopyOnClick';
const RowCheckbox = ({ id }: { id: number }) => { const RowCheckbox = ({ id }: { id: number }) => {
@ -93,7 +94,7 @@ const DatabasesContainer = () => {
<div css={tw`flex ml-auto pl-4`}> <div css={tw`flex ml-auto pl-4`}>
<NavLink to="/admin/databases/new"> <NavLink to="/admin/databases/new">
<Button type={'button'} size={'large'} css={tw`h-10 px-4 py-0 whitespace-nowrap`}> <Button type="button" size={Size.Large} css={tw`h-10 px-4 py-0 whitespace-nowrap`}>
New Database Host New Database Host
</Button> </Button>
</NavLink> </NavLink>

View file

@ -4,7 +4,8 @@ import { useState } from 'react';
import tw from 'twin.macro'; import tw from 'twin.macro';
import deleteLocation from '@/api/admin/locations/deleteLocation'; import deleteLocation from '@/api/admin/locations/deleteLocation';
import Button from '@/components/elements/Button'; import { Button } from '@/components/elements/button';
import { Shape } from '@/components/elements/button/types';
import ConfirmationModal from '@/components/elements/ConfirmationModal'; import ConfirmationModal from '@/components/elements/ConfirmationModal';
import type { ApplicationStore } from '@/state'; import type { ApplicationStore } from '@/state';
@ -53,7 +54,7 @@ export default ({ locationId, onDeleted }: Props) => {
to it. to it.
</ConfirmationModal> </ConfirmationModal>
<Button type={'button'} size={'xsmall'} color={'red'} onClick={() => setVisible(true)}> <Button.Danger type="button" shape={Shape.IconSquare} onClick={() => setVisible(true)}>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
@ -68,7 +69,7 @@ export default ({ locationId, onDeleted }: Props) => {
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/> />
</svg> </svg>
</Button> </Button.Danger>
</> </>
); );
}; };

View file

@ -13,7 +13,7 @@ import updateLocation from '@/api/admin/locations/updateLocation';
import AdminBox from '@/components/admin/AdminBox'; import AdminBox from '@/components/admin/AdminBox';
import AdminContentBlock from '@/components/admin/AdminContentBlock'; import AdminContentBlock from '@/components/admin/AdminContentBlock';
import LocationDeleteButton from '@/components/admin/locations/LocationDeleteButton'; import LocationDeleteButton from '@/components/admin/locations/LocationDeleteButton';
import Button from '@/components/elements/Button'; import { Button } from '@/components/elements/button';
import Field from '@/components/elements/Field'; import Field from '@/components/elements/Field';
import Spinner from '@/components/elements/Spinner'; import Spinner from '@/components/elements/Spinner';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
@ -99,7 +99,7 @@ const EditInformationContainer = () => {
</div> </div>
<div css={tw`flex ml-auto`}> <div css={tw`flex ml-auto`}>
<Button type={'submit'} disabled={isSubmitting || !isValid}> <Button type="submit" disabled={isSubmitting || !isValid}>
Save Changes Save Changes
</Button> </Button>
</div> </div>

View file

@ -24,7 +24,7 @@ import FlashMessageRender from '@/components/FlashMessageRender';
import useFlash from '@/plugins/useFlash'; import useFlash from '@/plugins/useFlash';
import { AdminContext } from '@/state/admin'; import { AdminContext } from '@/state/admin';
const RowCheckbox = ({ id }: { id: number }) => { function RowCheckbox({ id }: { id: number }) {
const isChecked = AdminContext.useStoreState(state => state.locations.selectedLocations.indexOf(id) >= 0); const isChecked = AdminContext.useStoreState(state => state.locations.selectedLocations.indexOf(id) >= 0);
const appendSelectedLocation = AdminContext.useStoreActions(actions => actions.locations.appendSelectedLocation); const appendSelectedLocation = AdminContext.useStoreActions(actions => actions.locations.appendSelectedLocation);
const removeSelectedLocation = AdminContext.useStoreActions(actions => actions.locations.removeSelectedLocation); const removeSelectedLocation = AdminContext.useStoreActions(actions => actions.locations.removeSelectedLocation);
@ -42,9 +42,9 @@ const RowCheckbox = ({ id }: { id: number }) => {
}} }}
/> />
); );
}; }
const LocationsContainer = () => { function LocationsContainer() {
const { page, setPage, setFilters, sort, setSort, sortDirection } = useContext(LocationsContext); const { page, setPage, setFilters, sort, setSort, sortDirection } = useContext(LocationsContext);
const { clearFlashes, clearAndAddHttpError } = useFlash(); const { clearFlashes, clearAndAddHttpError } = useFlash();
const { data: locations, error, isValidating } = getLocations(); const { data: locations, error, isValidating } = getLocations();
@ -173,7 +173,7 @@ const LocationsContainer = () => {
</AdminTable> </AdminTable>
</AdminContentBlock> </AdminContentBlock>
); );
}; }
export default () => { export default () => {
const hooks = useTableHooks<Filters>(); const hooks = useTableHooks<Filters>();

View file

@ -6,7 +6,8 @@ import { object, string } from 'yup';
import createLocation from '@/api/admin/locations/createLocation'; import createLocation from '@/api/admin/locations/createLocation';
import getLocations from '@/api/admin/locations/getLocations'; import getLocations from '@/api/admin/locations/getLocations';
import Button from '@/components/elements/Button'; import { Button } from '@/components/elements/button';
import { Size, Variant } from '@/components/elements/button/types';
import Field from '@/components/elements/Field'; import Field from '@/components/elements/Field';
import Modal from '@/components/elements/Modal'; import Modal from '@/components/elements/Modal';
import FlashMessageRender from '@/components/FlashMessageRender'; import FlashMessageRender from '@/components/FlashMessageRender';
@ -82,14 +83,14 @@ export default () => {
</div> </div>
<div css={tw`flex flex-wrap justify-end mt-6`}> <div css={tw`flex flex-wrap justify-end mt-6`}>
<Button <Button.Text
type={'button'} type="button"
isSecondary variant={Variant.Secondary}
css={tw`w-full sm:w-auto sm:mr-2`} css={tw`w-full sm:w-auto sm:mr-2`}
onClick={() => setVisible(false)} onClick={() => setVisible(false)}
> >
Cancel Cancel
</Button> </Button.Text>
<Button css={tw`w-full mt-4 sm:w-auto sm:mt-0`} type={'submit'}> <Button css={tw`w-full mt-4 sm:w-auto sm:mt-0`} type={'submit'}>
Create Location Create Location
</Button> </Button>
@ -100,8 +101,8 @@ export default () => {
</Formik> </Formik>
<Button <Button
type={'button'} type="button"
size={'large'} size={Size.Large}
css={tw`h-10 px-4 py-0 whitespace-nowrap`} css={tw`h-10 px-4 py-0 whitespace-nowrap`}
onClick={() => setVisible(true)} onClick={() => setVisible(true)}
> >

View file

@ -4,7 +4,7 @@ import { useState } from 'react';
import tw from 'twin.macro'; import tw from 'twin.macro';
import deleteMount from '@/api/admin/mounts/deleteMount'; import deleteMount from '@/api/admin/mounts/deleteMount';
import Button from '@/components/elements/Button'; import { Button } from '@/components/elements/button';
import ConfirmationModal from '@/components/elements/ConfirmationModal'; import ConfirmationModal from '@/components/elements/ConfirmationModal';
import type { ApplicationStore } from '@/state'; import type { ApplicationStore } from '@/state';
@ -52,7 +52,7 @@ export default ({ mountId, onDeleted }: Props) => {
Are you sure you want to delete this mount? Deleting a mount will not delete files on any nodes. Are you sure you want to delete this mount? Deleting a mount will not delete files on any nodes.
</ConfirmationModal> </ConfirmationModal>
<Button type={'button'} size={'xsmall'} color={'red'} onClick={() => setVisible(true)}> <Button.Danger type="button" onClick={() => setVisible(true)}>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
@ -67,7 +67,7 @@ export default ({ mountId, onDeleted }: Props) => {
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/> />
</svg> </svg>
</Button> </Button.Danger>
</> </>
); );
}; };

View file

@ -1,10 +1,11 @@
import type { FormikHelpers } from 'formik'; import type { FormikHelpers } from 'formik';
import { Field as FormikField, Form, Formik } from 'formik'; import { Field as FormikField, Form, Formik } from 'formik';
import type { ReactNode } from 'react';
import tw from 'twin.macro'; import tw from 'twin.macro';
import { boolean, object, string } from 'yup'; import { boolean, object, string } from 'yup';
import AdminBox from '@/components/admin/AdminBox'; import AdminBox from '@/components/admin/AdminBox';
import Button from '@/components/elements/Button'; import { Button } from '@/components/elements/button';
import Field from '@/components/elements/Field'; import Field from '@/components/elements/Field';
import Label from '@/components/elements/Label'; import Label from '@/components/elements/Label';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
@ -25,7 +26,7 @@ interface Props {
onSubmit: (values: Values, helpers: FormikHelpers<Values>) => void; onSubmit: (values: Values, helpers: FormikHelpers<Values>) => void;
children?: React.ReactNode; children?: ReactNode;
} }
function MountForm({ action, title, initialValues, children, onSubmit }: Props) { function MountForm({ action, title, initialValues, children, onSubmit }: Props) {
@ -118,7 +119,7 @@ function MountForm({ action, title, initialValues, children, onSubmit }: Props)
{children} {children}
<div css={tw`flex ml-auto`}> <div css={tw`flex ml-auto`}>
<Button type={'submit'} disabled={isSubmitting || !isValid}> <Button type="submit" disabled={isSubmitting || !isValid}>
{action} {action}
</Button> </Button>
</div> </div>

View file

@ -18,7 +18,8 @@ import AdminTable, {
ContentWrapper, ContentWrapper,
useTableHooks, useTableHooks,
} from '@/components/admin/AdminTable'; } from '@/components/admin/AdminTable';
import Button from '@/components/elements/Button'; import { Button } from '@/components/elements/button';
import { Size } from '@/components/elements/button/types';
import CopyOnClick from '@/components/elements/CopyOnClick'; import CopyOnClick from '@/components/elements/CopyOnClick';
import FlashMessageRender from '@/components/FlashMessageRender'; import FlashMessageRender from '@/components/FlashMessageRender';
import useFlash from '@/plugins/useFlash'; import useFlash from '@/plugins/useFlash';
@ -94,7 +95,7 @@ const MountsContainer = () => {
<div css={tw`flex ml-auto pl-4`}> <div css={tw`flex ml-auto pl-4`}>
<NavLink to={`/admin/mounts/new`}> <NavLink to={`/admin/mounts/new`}>
<Button type={'button'} size={'large'} css={tw`h-10 px-4 py-0 whitespace-nowrap`}> <Button type={'button'} size={Size.Large} css={tw`h-10 px-4 py-0 whitespace-nowrap`}>
New Mount New Mount
</Button> </Button>
</NavLink> </NavLink>

View file

@ -1,13 +1,17 @@
import { LanguageDescription } from '@codemirror/language';
import { json } from '@codemirror/lang-json';
import { useState } from 'react';
import { useParams } from 'react-router-dom';
import tw from 'twin.macro';
import getEggs from '@/api/admin/nests/getEggs'; import getEggs from '@/api/admin/nests/getEggs';
import importEgg from '@/api/admin/nests/importEgg'; import importEgg from '@/api/admin/nests/importEgg';
import useFlash from '@/plugins/useFlash'; import useFlash from '@/plugins/useFlash';
// import { Editor } from '@/components/elements/editor'; import { Button } from '@/components/elements/button';
import { useState } from 'react'; import { Size, Variant } from '@/components/elements/button/types';
import Button from '@/components/elements/Button'; import { Editor } from '@/components/elements/editor';
import Modal from '@/components/elements/Modal'; import Modal from '@/components/elements/Modal';
import FlashMessageRender from '@/components/FlashMessageRender'; import FlashMessageRender from '@/components/FlashMessageRender';
import { useParams } from 'react-router-dom';
import tw from 'twin.macro';
export default ({ className }: { className?: string }) => { export default ({ className }: { className?: string }) => {
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
@ -43,24 +47,24 @@ export default ({ className }: { className?: string }) => {
<h2 css={tw`mb-6 text-2xl text-neutral-100`}>Import Egg</h2> <h2 css={tw`mb-6 text-2xl text-neutral-100`}>Import Egg</h2>
{/*<Editor*/} <Editor
{/* // overrides={tw`h-64 rounded`}*/} className="h-64 overflow-hidden rounded"
{/* initialContent={''}*/} initialContent={''}
{/* // language={jsonLanguage}*/} fetchContent={value => {
{/* fetchContent={value => {*/} fetchFileContent = value;
{/* fetchFileContent = value;*/} }}
{/* }}*/} language={LanguageDescription.of({ name: 'json', support: json() })}
{/*/>*/} />
<div css={tw`flex flex-wrap justify-end mt-4 sm:mt-6`}> <div css={tw`flex flex-wrap justify-end mt-4 sm:mt-6`}>
<Button <Button.Text
type={'button'} type="button"
variant={Variant.Secondary}
css={tw`w-full sm:w-auto sm:mr-2`} css={tw`w-full sm:w-auto sm:mr-2`}
onClick={() => setVisible(false)} onClick={() => setVisible(false)}
isSecondary
> >
Cancel Cancel
</Button> </Button.Text>
<Button css={tw`w-full sm:w-auto mt-4 sm:mt-0`} onClick={submit}> <Button css={tw`w-full sm:w-auto mt-4 sm:mt-0`} onClick={submit}>
Import Egg Import Egg
</Button> </Button>
@ -68,12 +72,12 @@ export default ({ className }: { className?: string }) => {
</Modal> </Modal>
<Button <Button
type={'button'} type="button"
size={'large'} size={Size.Large}
variant={Variant.Secondary}
css={tw`h-10 px-4 py-0 whitespace-nowrap`} css={tw`h-10 px-4 py-0 whitespace-nowrap`}
className={className} className={className}
onClick={() => setVisible(true)} onClick={() => setVisible(true)}
isSecondary
> >
Import Import
</Button> </Button>

View file

@ -4,7 +4,8 @@ import { useState } from 'react';
import tw from 'twin.macro'; import tw from 'twin.macro';
import deleteNest from '@/api/admin/nests/deleteNest'; import deleteNest from '@/api/admin/nests/deleteNest';
import Button from '@/components/elements/Button'; import { Button } from '@/components/elements/button';
import { Shape } from '@/components/elements/button/types';
import ConfirmationModal from '@/components/elements/ConfirmationModal'; import ConfirmationModal from '@/components/elements/ConfirmationModal';
import type { ApplicationStore } from '@/state'; import type { ApplicationStore } from '@/state';
@ -52,7 +53,7 @@ export default ({ nestId, onDeleted }: Props) => {
Are you sure you want to delete this nest? Deleting a nest will delete all eggs assigned to it. Are you sure you want to delete this nest? Deleting a nest will delete all eggs assigned to it.
</ConfirmationModal> </ConfirmationModal>
<Button type={'button'} size={'xsmall'} color={'red'} onClick={() => setVisible(true)}> <Button.Danger type="button" shape={Shape.IconSquare} onClick={() => setVisible(true)}>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
@ -67,7 +68,7 @@ export default ({ nestId, onDeleted }: Props) => {
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/> />
</svg> </svg>
</Button> </Button.Danger>
</> </>
); );
}; };

View file

@ -14,7 +14,8 @@ import FlashMessageRender from '@/components/FlashMessageRender';
import type { Nest } from '@/api/admin/nests/getNests'; import type { Nest } from '@/api/admin/nests/getNests';
import getNest from '@/api/admin/nests/getNest'; import getNest from '@/api/admin/nests/getNest';
import updateNest from '@/api/admin/nests/updateNest'; import updateNest from '@/api/admin/nests/updateNest';
import Button from '@/components/elements/Button'; import { Button } from '@/components/elements/button';
import { Size } from '@/components/elements/button/types';
import Field from '@/components/elements/Field'; import Field from '@/components/elements/Field';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
import AdminBox from '@/components/admin/AdminBox'; import AdminBox from '@/components/admin/AdminBox';
@ -117,7 +118,7 @@ const EditInformationContainer = () => {
</div> </div>
<div css={tw`flex ml-auto`}> <div css={tw`flex ml-auto`}>
<Button type={'submit'} disabled={isSubmitting || !isValid}> <Button type="submit" disabled={isSubmitting || !isValid}>
Save Changes Save Changes
</Button> </Button>
</div> </div>
@ -222,7 +223,7 @@ const NestEditContainer = () => {
<ImportEggButton css={tw`mr-4`} /> <ImportEggButton css={tw`mr-4`} />
<NavLink to={`/admin/nests/${params.nestId}/new`}> <NavLink to={`/admin/nests/${params.nestId}/new`}>
<Button type={'button'} size={'large'} css={tw`h-10 px-4 py-0 whitespace-nowrap`}> <Button type={'button'} size={Size.Large} css={tw`h-10 px-4 py-0 whitespace-nowrap`}>
New Egg New Egg
</Button> </Button>
</NavLink> </NavLink>

View file

@ -15,7 +15,7 @@ import {
EggProcessContainerRef, EggProcessContainerRef,
EggStartupContainer, EggStartupContainer,
} from '@/components/admin/nests/eggs/EggSettingsContainer'; } from '@/components/admin/nests/eggs/EggSettingsContainer';
import Button from '@/components/elements/Button'; import { Button } from '@/components/elements/button';
import FlashMessageRender from '@/components/FlashMessageRender'; import FlashMessageRender from '@/components/FlashMessageRender';
import useFlash from '@/plugins/useFlash'; import useFlash from '@/plugins/useFlash';
@ -45,7 +45,16 @@ export default () => {
values.configStartup = (await ref.current?.getStartupConfiguration()) || ''; values.configStartup = (await ref.current?.getStartupConfiguration()) || '';
values.configFiles = (await ref.current?.getFilesConfiguration()) || ''; values.configFiles = (await ref.current?.getFilesConfiguration()) || '';
createEgg({ ...values, dockerImages: values.dockerImages.split('\n'), nestId }) const dockerImages: Record<string, string> = {};
values.dockerImages.split('\n').forEach(v => {
dockerImages[v] = v;
});
createEgg({
...values,
dockerImages,
nestId,
})
.then(egg => navigate(`/admin/nests/${nestId}/eggs/${egg.id}`)) .then(egg => navigate(`/admin/nests/${nestId}/eggs/${egg.id}`))
.catch(error => { .catch(error => {
console.error(error); console.error(error);
@ -97,12 +106,7 @@ export default () => {
<div css={tw`bg-neutral-700 rounded shadow-md py-2 px-6 mb-16`}> <div css={tw`bg-neutral-700 rounded shadow-md py-2 px-6 mb-16`}>
<div css={tw`flex flex-row`}> <div css={tw`flex flex-row`}>
<Button <Button type="submit" css={tw`ml-auto`} disabled={isSubmitting || !isValid}>
type="submit"
size="small"
css={tw`ml-auto`}
disabled={isSubmitting || !isValid}
>
Create Create
</Button> </Button>
</div> </div>

View file

@ -1,4 +1,9 @@
import React, { useState } from 'react'; import type { FormikHelpers } from 'formik';
import { Form, Formik } from 'formik';
import { useState } from 'react';
import tw from 'twin.macro';
import { object, string } from 'yup';
import createNest from '@/api/admin/nests/createNest'; import createNest from '@/api/admin/nests/createNest';
import getNests from '@/api/admin/nests/getNests'; import getNests from '@/api/admin/nests/getNests';
import Button from '@/components/elements/Button'; import Button from '@/components/elements/Button';
@ -6,21 +11,15 @@ import Field from '@/components/elements/Field';
import Modal from '@/components/elements/Modal'; import Modal from '@/components/elements/Modal';
import FlashMessageRender from '@/components/FlashMessageRender'; import FlashMessageRender from '@/components/FlashMessageRender';
import useFlash from '@/plugins/useFlash'; import useFlash from '@/plugins/useFlash';
import { Form, Formik, FormikHelpers } from 'formik';
import { object, string } from 'yup';
import tw from 'twin.macro';
interface Values { interface Values {
name: string, name: string;
description: string, description: string;
} }
const schema = object().shape({ const schema = object().shape({
name: string() name: string().required('A nest name must be provided.').max(32, 'Nest name must not exceed 32 characters.'),
.required('A nest name must be provided.') description: string().max(255, 'Nest description must not exceed 255 characters.'),
.max(32, 'Nest name must not exceed 32 characters.'),
description: string()
.max(255, 'Nest description must not exceed 255 characters.'),
}); });
export default () => { export default () => {
@ -33,7 +32,7 @@ export default () => {
setSubmitting(true); setSubmitting(true);
createNest(name, description) createNest(name, description)
.then(async (nest) => { .then(async nest => {
await mutate(data => ({ ...data!, items: data!.items.concat(nest) }), false); await mutate(data => ({ ...data!, items: data!.items.concat(nest) }), false);
setVisible(false); setVisible(false);
}) })
@ -45,13 +44,8 @@ export default () => {
return ( return (
<> <>
<Formik <Formik onSubmit={submit} initialValues={{ name: '', description: '' }} validationSchema={schema}>
onSubmit={submit} {({ isSubmitting, resetForm }) => (
initialValues={{ name: '', description: '' }}
validationSchema={schema}
>
{
({ isSubmitting, resetForm }) => (
<Modal <Modal
visible={visible} visible={visible}
dismissable={!isSubmitting} dismissable={!isSubmitting}
@ -100,11 +94,15 @@ export default () => {
</div> </div>
</Form> </Form>
</Modal> </Modal>
) )}
}
</Formik> </Formik>
<Button type={'button'} size={'large'} css={tw`h-10 px-4 py-0 whitespace-nowrap`} onClick={() => setVisible(true)}> <Button
type={'button'}
size={'large'}
css={tw`h-10 px-4 py-0 whitespace-nowrap`}
onClick={() => setVisible(true)}
>
New Nest New Nest
</Button> </Button>
</> </>

View file

@ -4,7 +4,8 @@ import { useState } from 'react';
import tw from 'twin.macro'; import tw from 'twin.macro';
import deleteEgg from '@/api/admin/eggs/deleteEgg'; import deleteEgg from '@/api/admin/eggs/deleteEgg';
import Button from '@/components/elements/Button'; import { Button } from '@/components/elements/button';
import { Shape } from '@/components/elements/button/types';
import ConfirmationModal from '@/components/elements/ConfirmationModal'; import ConfirmationModal from '@/components/elements/ConfirmationModal';
import type { ApplicationStore } from '@/state'; import type { ApplicationStore } from '@/state';
@ -52,7 +53,7 @@ export default ({ eggId, onDeleted }: Props) => {
Are you sure you want to delete this egg? You may only delete an egg with no servers using it. Are you sure you want to delete this egg? You may only delete an egg with no servers using it.
</ConfirmationModal> </ConfirmationModal>
<Button type={'button'} size={'xsmall'} color={'red'} onClick={() => setVisible(true)}> <Button.Danger type="button" shape={Shape.IconSquare} onClick={() => setVisible(true)}>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
@ -67,7 +68,7 @@ export default ({ eggId, onDeleted }: Props) => {
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/> />
</svg> </svg>
</Button> </Button.Danger>
</> </>
); );
}; };

View file

@ -1,22 +1,25 @@
import { exportEgg } from '@/api/admin/egg'; import { LanguageDescription } from '@codemirror/language';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; import { json } from '@codemirror/lang-json';
import useFlash from '@/plugins/useFlash';
// import { jsonLanguage } from '@codemirror/lang-json';
// import Editor from '@/components/elements/Editor';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import Button from '@/components/elements/Button';
import Modal from '@/components/elements/Modal';
import FlashMessageRender from '@/components/FlashMessageRender';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import tw from 'twin.macro'; import tw from 'twin.macro';
import { exportEgg } from '@/api/admin/egg';
import FlashMessageRender from '@/components/FlashMessageRender';
import { Button } from '@/components/elements/button';
import { Variant } from '@/components/elements/button/types';
import { Editor } from '@/components/elements/editor';
import Modal from '@/components/elements/Modal';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
import useFlash from '@/plugins/useFlash';
export default ({ className }: { className?: string }) => { export default ({ className }: { className?: string }) => {
const params = useParams<'id'>(); const params = useParams<'id'>();
const { clearAndAddHttpError, clearFlashes } = useFlash(); const { clearAndAddHttpError, clearFlashes } = useFlash();
const [visible, setVisible] = useState<boolean>(false); const [visible, setVisible] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(true); const [loading, setLoading] = useState<boolean>(true);
const [_content, setContent] = useState<Record<string, any> | null>(null); const [content, setContent] = useState<string | undefined>(undefined);
useEffect(() => { useEffect(() => {
if (!visible) { if (!visible) {
@ -45,21 +48,22 @@ export default ({ className }: { className?: string }) => {
<h2 css={tw`mb-6 text-2xl text-neutral-100`}>Export Egg</h2> <h2 css={tw`mb-6 text-2xl text-neutral-100`}>Export Egg</h2>
<FlashMessageRender byKey={'egg:export'} css={tw`mb-6`} /> <FlashMessageRender byKey={'egg:export'} css={tw`mb-6`} />
{/*<Editor*/} <Editor
{/* overrides={tw`h-[32rem] rounded`}*/} className="h-[32rem] overflow-scroll rounded"
{/* initialContent={content !== null ? JSON.stringify(content, null, '\t') : ''}*/} initialContent={content ?? ''}
{/* mode={jsonLanguage}*/} language={LanguageDescription.of({ name: 'json', support: json() })}
{/*/>*/} />
<div css={tw`flex flex-wrap justify-end mt-4 sm:mt-6`}> <div css={tw`flex flex-wrap justify-end mt-4 sm:mt-6`}>
<Button <Button.Text
type={'button'} type="button"
variant={Variant.Secondary}
css={tw`w-full sm:w-auto sm:mr-2`} css={tw`w-full sm:w-auto sm:mr-2`}
onClick={() => setVisible(false)} onClick={() => setVisible(false)}
isSecondary
> >
Close Close
</Button> </Button.Text>
<Button <Button
css={tw`w-full sm:w-auto mt-4 sm:mt-0`} css={tw`w-full sm:w-auto mt-4 sm:mt-0`}
// onClick={submit} // onClick={submit}
@ -70,16 +74,14 @@ export default ({ className }: { className?: string }) => {
</div> </div>
</Modal> </Modal>
<Button <Button.Text
type={'button'} type="button"
size={'small'}
css={tw`px-4 py-0 whitespace-nowrap`} css={tw`px-4 py-0 whitespace-nowrap`}
className={className} className={className}
onClick={() => setVisible(true)} onClick={() => setVisible(true)}
isSecondary
> >
Export Export
</Button> </Button.Text>
</> </>
); );
}; };

View file

@ -1,15 +1,18 @@
import { LanguageDescription, LanguageSupport, StreamLanguage } from '@codemirror/language';
import { shell } from '@codemirror/legacy-modes/mode/shell';
import { faScroll } from '@fortawesome/free-solid-svg-icons';
import type { FormikHelpers } from 'formik';
import { Form, Formik } from 'formik';
import tw from 'twin.macro';
import { useEggFromRoute } from '@/api/admin/egg'; import { useEggFromRoute } from '@/api/admin/egg';
import updateEgg from '@/api/admin/eggs/updateEgg'; import updateEgg from '@/api/admin/eggs/updateEgg';
import Field from '@/components/elements/Field';
import useFlash from '@/plugins/useFlash';
// import { shell } from '@codemirror/legacy-modes/mode/shell';
import { faScroll } from '@fortawesome/free-solid-svg-icons';
import { Form, Formik, FormikHelpers } from 'formik';
import tw from 'twin.macro';
import AdminBox from '@/components/admin/AdminBox'; import AdminBox from '@/components/admin/AdminBox';
import Button from '@/components/elements/Button'; import { Button } from '@/components/elements/button';
// import Editor from '@/components/elements/Editor'; import { Editor } from '@/components/elements/editor';
import Field from '@/components/elements/Field';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
import useFlash from '@/plugins/useFlash';
interface Values { interface Values {
scriptContainer: string; scriptContainer: string;
@ -60,14 +63,17 @@ export default function EggInstallContainer() {
<SpinnerOverlay visible={isSubmitting} /> <SpinnerOverlay visible={isSubmitting} />
<Form> <Form>
{/*<Editor*/} <Editor
{/* overrides={tw`h-96 mb-4`}*/} className="mb-4 h-96 overflow-scroll"
{/* initialContent={egg.scriptInstall || ''}*/} initialContent={egg.scriptInstall || ''}
{/* mode={shell}*/} fetchContent={value => {
{/* fetchContent={value => {*/} fetchFileContent = value;
{/* fetchFileContent = value;*/} }}
{/* }}*/} language={LanguageDescription.of({
{/*/>*/} name: 'shell',
support: new LanguageSupport(StreamLanguage.define(shell)),
})}
/>
<div css={tw`mx-6 mb-4`}> <div css={tw`mx-6 mb-4`}>
<div css={tw`grid grid-cols-3 gap-x-8 gap-y-6`}> <div css={tw`grid grid-cols-3 gap-x-8 gap-y-6`}>
@ -92,12 +98,7 @@ export default function EggInstallContainer() {
</div> </div>
<div css={tw`flex flex-row border-t border-neutral-600`}> <div css={tw`flex flex-row border-t border-neutral-600`}>
<Button <Button type="submit" css={tw`ml-auto mr-6 mt-4`} disabled={isSubmitting || !isValid}>
type={'submit'}
size={'small'}
css={tw`ml-auto mr-6 mt-4`}
disabled={isSubmitting || !isValid}
>
Save Changes Save Changes
</Button> </Button>
</div> </div>

View file

@ -1,23 +1,26 @@
import { LanguageDescription } from '@codemirror/language';
import { json } from '@codemirror/lang-json';
import { faDocker } from '@fortawesome/free-brands-svg-icons';
import { faEgg, faFireAlt, faMicrochip, faTerminal } from '@fortawesome/free-solid-svg-icons';
import type { FormikHelpers } from 'formik';
import { Form, Formik, useFormikContext } from 'formik';
import { forwardRef, useImperativeHandle, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import tw from 'twin.macro';
import { object } from 'yup';
import { useEggFromRoute } from '@/api/admin/egg'; import { useEggFromRoute } from '@/api/admin/egg';
import updateEgg from '@/api/admin/eggs/updateEgg'; import updateEgg from '@/api/admin/eggs/updateEgg';
import AdminBox from '@/components/admin/AdminBox';
import EggDeleteButton from '@/components/admin/nests/eggs/EggDeleteButton'; import EggDeleteButton from '@/components/admin/nests/eggs/EggDeleteButton';
import EggExportButton from '@/components/admin/nests/eggs/EggExportButton'; import EggExportButton from '@/components/admin/nests/eggs/EggExportButton';
import Button from '@/components/elements/Button'; import { Button } from '@/components/elements/button';
// import Editor from '@/components/elements/Editor'; import { Editor } from '@/components/elements/editor';
import Field, { TextareaField } from '@/components/elements/Field'; import Field, { TextareaField } from '@/components/elements/Field';
import Input from '@/components/elements/Input'; import Input from '@/components/elements/Input';
import Label from '@/components/elements/Label'; import Label from '@/components/elements/Label';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
import useFlash from '@/plugins/useFlash'; import useFlash from '@/plugins/useFlash';
// import { jsonLanguage } from '@codemirror/lang-json';
import { faDocker } from '@fortawesome/free-brands-svg-icons';
import { faEgg, faFireAlt, faMicrochip, faTerminal } from '@fortawesome/free-solid-svg-icons';
import { forwardRef, useImperativeHandle, useRef } from 'react';
import AdminBox from '@/components/admin/AdminBox';
import { useNavigate } from 'react-router-dom';
import tw from 'twin.macro';
import { object } from 'yup';
import { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
export function EggInformationContainer() { export function EggInformationContainer() {
const { isSubmitting } = useFormikContext(); const { isSubmitting } = useFormikContext();
@ -104,8 +107,7 @@ export const EggProcessContainer = forwardRef<any, EggProcessContainerProps>(fun
{ className }, { className },
ref, ref,
) { ) {
// const { isSubmitting, values } = useFormikContext<Values>(); const { isSubmitting, values } = useFormikContext<Values>();
const { isSubmitting } = useFormikContext<Values>();
let fetchStartupConfiguration: (() => Promise<string>) | null = null; let fetchStartupConfiguration: (() => Promise<string>) | null = null;
let fetchFilesConfiguration: (() => Promise<string>) | null = null; let fetchFilesConfiguration: (() => Promise<string>) | null = null;
@ -132,26 +134,26 @@ export const EggProcessContainer = forwardRef<any, EggProcessContainerProps>(fun
<div css={tw`mb-5`}> <div css={tw`mb-5`}>
<Label>Startup Configuration</Label> <Label>Startup Configuration</Label>
{/*<Editor*/} <Editor
{/* mode={jsonLanguage}*/} className="h-32 overflow-scroll rounded"
{/* initialContent={values.configStartup}*/} initialContent={values.configStartup}
{/* overrides={tw`h-32 rounded`}*/} fetchContent={value => {
{/* fetchContent={value => {*/} fetchStartupConfiguration = value;
{/* fetchStartupConfiguration = value;*/} }}
{/* }}*/} language={LanguageDescription.of({ name: 'json', support: json() })}
{/*/>*/} />
</div> </div>
<div css={tw`mb-1`}> <div css={tw`mb-1`}>
<Label>Configuration Files</Label> <Label>Configuration Files</Label>
{/*<Editor*/} <Editor
{/* mode={jsonLanguage}*/} className="h-48 overflow-scroll rounded"
{/* initialContent={values.configFiles}*/} initialContent={values.configFiles}
{/* overrides={tw`h-48 rounded`}*/} fetchContent={value => {
{/* fetchContent={value => {*/} fetchFilesConfiguration = value;
{/* fetchFilesConfiguration = value;*/} }}
{/* }}*/} language={LanguageDescription.of({ name: 'json', support: json() })}
{/*/>*/} />
</div> </div>
</AdminBox> </AdminBox>
); );
@ -233,7 +235,7 @@ export default function EggSettingsContainer() {
<div css={tw`flex flex-row`}> <div css={tw`flex flex-row`}>
<EggDeleteButton eggId={egg.id} onDeleted={() => navigate('/admin/nests')} /> <EggDeleteButton eggId={egg.id} onDeleted={() => navigate('/admin/nests')} />
<EggExportButton css={tw`ml-auto mr-4`} /> <EggExportButton css={tw`ml-auto mr-4`} />
<Button type="submit" size="small" disabled={isSubmitting || !isValid}> <Button type="submit" disabled={isSubmitting || !isValid}>
Save Changes Save Changes
</Button> </Button>
</div> </div>

View file

@ -13,11 +13,12 @@ import type { EggVariable } from '@/api/admin/egg';
import { useEggFromRoute } from '@/api/admin/egg'; import { useEggFromRoute } from '@/api/admin/egg';
import NewVariableButton from '@/components/admin/nests/eggs/NewVariableButton'; import NewVariableButton from '@/components/admin/nests/eggs/NewVariableButton';
import AdminBox from '@/components/admin/AdminBox'; import AdminBox from '@/components/admin/AdminBox';
import Button from '@/components/elements/Button'; import { Button } from '@/components/elements/button';
import Checkbox from '@/components/elements/Checkbox'; import Checkbox from '@/components/elements/Checkbox';
import Field, { FieldRow, TextareaField } from '@/components/elements/Field'; import Field, { FieldRow, TextareaField } from '@/components/elements/Field';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
import useFlash from '@/plugins/useFlash'; import useFlash from '@/plugins/useFlash';
import Label from '@/components/elements/Label';
export const validationSchema = object().shape({ export const validationSchema = object().shape({
name: string().required().min(1).max(191), name: string().required().min(1).max(191),
@ -59,14 +60,23 @@ export function EggVariableForm({ prefix }: { prefix: string }) {
</FieldRow> </FieldRow>
<div css={tw`flex flex-row mb-6`}> <div css={tw`flex flex-row mb-6`}>
<Checkbox id={`${prefix}isUserViewable`} name={`${prefix}isUserViewable`} label={'User Viewable'} /> <div className="ml-auto flex flex-row">
{/* TODO: fix Checkbox component, current one is designed for subuser permissions and not for individual values */}
<Checkbox id={`${prefix}isUserViewable`} name={`${prefix}isUserViewable`} />
<Checkbox <div css={tw`flex-1`}>
id={`${prefix}isUserEditable`} <Label>User Viewable</Label>
name={`${prefix}isUserEditable`} </div>
label={'User Editable'} </div>
css={tw`ml-auto`}
/> <div className="ml-auto flex flex-row">
{/* TODO: fix Checkbox component, current one is designed for subuser permissions and not for individual values */}
<Checkbox id={`${prefix}isUserEditable`} name={`${prefix}isUserEditable`} />
<div css={tw`flex-1`}>
<Label>User Editable</Label>
</div>
</div>
</div> </div>
<Field <Field
@ -111,7 +121,7 @@ function EggVariableDeleteButton({ onClick }: { onClick: (success: () => void) =
css={tw`ml-auto text-neutral-500 hover:text-neutral-300`} css={tw`ml-auto text-neutral-500 hover:text-neutral-300`}
onClick={() => setVisible(true)} onClick={() => setVisible(true)}
> >
<TrashIcon css={tw`h-5 w-5`} /> <TrashIcon className="h-5 w-5" />
</button> </button>
</> </>
); );
@ -200,12 +210,7 @@ export default function EggVariablesContainer() {
<div css={tw`flex flex-row`}> <div css={tw`flex flex-row`}>
<NewVariableButton /> <NewVariableButton />
<Button <Button type="submit" className="ml-auto" disabled={isSubmitting || !isValid}>
type={'submit'}
size={'small'}
css={tw`ml-auto`}
disabled={isSubmitting || !isValid}
>
Save Changes Save Changes
</Button> </Button>
</div> </div>

View file

@ -9,8 +9,9 @@ import { useEggFromRoute } from '@/api/admin/egg';
import { EggVariableForm, validationSchema } from '@/components/admin/nests/eggs/EggVariablesContainer'; import { EggVariableForm, validationSchema } from '@/components/admin/nests/eggs/EggVariablesContainer';
import Modal from '@/components/elements/Modal'; import Modal from '@/components/elements/Modal';
import FlashMessageRender from '@/components/FlashMessageRender'; import FlashMessageRender from '@/components/FlashMessageRender';
import Button from '@/components/elements/Button'; import { Button } from '@/components/elements/button';
import useFlash from '@/plugins/useFlash'; import useFlash from '@/plugins/useFlash';
import { Variant } from '@/components/elements/button/types';
export default function NewVariableButton() { export default function NewVariableButton() {
const { setValues } = useFormikContext(); const { setValues } = useFormikContext();
@ -75,16 +76,16 @@ export default function NewVariableButton() {
<div css={tw`flex flex-wrap justify-end mt-6`}> <div css={tw`flex flex-wrap justify-end mt-6`}>
<Button <Button
type={'button'} type="button"
isSecondary variant={Variant.Secondary}
css={tw`w-full sm:w-auto sm:mr-2`} css={tw`w-full sm:w-auto sm:mr-2`}
onClick={() => setVisible(false)} onClick={() => setVisible(false)}
> >
Cancel Cancel
</Button> </Button>
<Button <Button
type="submit"
css={tw`w-full mt-4 sm:w-auto sm:mt-0`} css={tw`w-full mt-4 sm:w-auto sm:mt-0`}
type={'submit'}
disabled={isSubmitting || !isValid} disabled={isSubmitting || !isValid}
> >
Create Variable Create Variable
@ -95,7 +96,8 @@ export default function NewVariableButton() {
)} )}
</Formik> </Formik>
<Button type={'button'} color={'green'} onClick={() => setVisible(true)}> {/* TODO: make button green */}
<Button type="button" onClick={() => setVisible(true)}>
New Variable New Variable
</Button> </Button>
</> </>

View file

@ -4,7 +4,6 @@ import { useState } from 'react';
import Checkbox from '@/components/elements/inputs/Checkbox'; import Checkbox from '@/components/elements/inputs/Checkbox';
import { Dropdown } from '@/components/elements/dropdown'; import { Dropdown } from '@/components/elements/dropdown';
import { Dialog } from '@/components/elements/dialog'; import { Dialog } from '@/components/elements/dialog';
import { Button } from '@/components/elements/button';
import { User } from '@definitions/admin'; import { User } from '@definitions/admin';
interface Props { interface Props {
@ -18,14 +17,17 @@ const UserTableRow = ({ user, selected, onRowChange }: Props) => {
return ( return (
<> <>
<Dialog title={'Delete account'} visible={visible} onDismissed={() => setVisible(false)}> <Dialog.Confirm
<Dialog.Icon type={'danger'} /> title={'Delete account'}
open={visible}
onClose={() => setVisible(false)}
onConfirmed={() => {
console.log('yeet');
}}
>
This account will be permanently deleted. This account will be permanently deleted.
<Dialog.Buttons> </Dialog.Confirm>
<Button.Text onClick={() => setVisible(false)}>Cancel</Button.Text>
<Button.Danger>Delete</Button.Danger>
</Dialog.Buttons>
</Dialog>
<tr> <tr>
<td className={'whitespace-nowrap'}> <td className={'whitespace-nowrap'}>
<div className={'flex justify-end items-center w-8'}> <div className={'flex justify-end items-center w-8'}>

View file

@ -12,6 +12,7 @@ import TFootPaginated from '@/components/elements/table/TFootPaginated';
import type { User } from '@definitions/admin'; import type { User } from '@definitions/admin';
import extractSearchFilters from '@/helpers/extractSearchFilters'; import extractSearchFilters from '@/helpers/extractSearchFilters';
import useDebouncedState from '@/plugins/useDebouncedState'; import useDebouncedState from '@/plugins/useDebouncedState';
import { Shape } from '@/components/elements/button/types';
const filters = ['id', 'uuid', 'external_id', 'username', 'email'] as const; const filters = ['id', 'uuid', 'external_id', 'username', 'email'] as const;
@ -77,13 +78,13 @@ const UsersContainer = () => {
onChange={onSelectAll} onChange={onSelectAll}
/> />
</div> </div>
<Button.Text square> <Button.Text shape={Shape.IconSquare}>
<SupportIcon className={'w-4 h-4'} /> <SupportIcon className={'w-4 h-4'} />
</Button.Text> </Button.Text>
<Button.Text square> <Button.Text shape={Shape.IconSquare}>
<LockOpenIcon className={'w-4 h-4'} /> <LockOpenIcon className={'w-4 h-4'} />
</Button.Text> </Button.Text>
<Button.Text square> <Button.Text shape={Shape.IconSquare}>
<TrashIcon className={'w-4 h-4'} /> <TrashIcon className={'w-4 h-4'} />
</Button.Text> </Button.Text>
</div> </div>

View file

@ -1,9 +1,13 @@
import { Field, FieldProps } from 'formik'; import type { FieldProps } from 'formik';
import { Field } from 'formik';
import Input from '@/components/elements/Input'; import Input from '@/components/elements/Input';
interface Props { interface Props {
id: string;
name: string; name: string;
value: string; value?: string;
label?: string;
className?: string; className?: string;
} }

View file

@ -1,318 +0,0 @@
import { autocompletion, completionKeymap } from '@codemirror/autocomplete';
import { closeBrackets, closeBracketsKeymap } from '@codemirror/closebrackets';
import { defaultKeymap, indentWithTab } from '@codemirror/commands';
import { commentKeymap } from '@codemirror/comment';
import { foldGutter, foldKeymap } from '@codemirror/fold';
import { lineNumbers, highlightActiveLineGutter } from '@codemirror/gutter';
import { defaultHighlightStyle } from '@codemirror/highlight';
import { history, historyKeymap } from '@codemirror/history';
import { indentOnInput, LanguageSupport, LRLanguage, indentUnit } from '@codemirror/language';
import { lintKeymap } from '@codemirror/lint';
import { bracketMatching } from '@codemirror/matchbrackets';
import { rectangularSelection } from '@codemirror/rectangular-selection';
import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
import { Compartment, Extension, EditorState } from '@codemirror/state';
import { StreamLanguage, StreamParser } from '@codemirror/stream-parser';
import { keymap, highlightSpecialChars, drawSelection, highlightActiveLine, EditorView } from '@codemirror/view';
import { clike } from '@codemirror/legacy-modes/mode/clike';
import { cpp } from '@codemirror/lang-cpp';
import { css } from '@codemirror/lang-css';
import { Cassandra, MariaSQL, MSSQL, MySQL, PostgreSQL, sql, SQLite, StandardSQL } from '@codemirror/lang-sql';
import { diff } from '@codemirror/legacy-modes/mode/diff';
import { dockerFile } from '@codemirror/legacy-modes/mode/dockerfile';
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
import { go } from '@codemirror/legacy-modes/mode/go';
import { html } from '@codemirror/lang-html';
import { http } from '@codemirror/legacy-modes/mode/http';
import { javascript, typescriptLanguage } from '@codemirror/lang-javascript';
import { json } from '@codemirror/lang-json';
import { lua } from '@codemirror/legacy-modes/mode/lua';
import { properties } from '@codemirror/legacy-modes/mode/properties';
import { python } from '@codemirror/legacy-modes/mode/python';
import { ruby } from '@codemirror/legacy-modes/mode/ruby';
import { rust } from '@codemirror/lang-rust';
import { shell } from '@codemirror/legacy-modes/mode/shell';
import { toml } from '@codemirror/legacy-modes/mode/toml';
import { xml } from '@codemirror/lang-xml';
import { yaml } from '@codemirror/legacy-modes/mode/yaml';
import React, { useCallback, useEffect, useState } from 'react';
import tw, { styled, TwStyle } from 'twin.macro';
import { ayuMirage } from '@/components/elements/EditorTheme';
type EditorMode = LanguageSupport | LRLanguage | StreamParser<unknown>;
export interface Mode {
name: string;
mime: string;
mimes?: string[];
mode?: EditorMode;
ext?: string[];
alias?: string[];
file?: RegExp;
}
export const modes: Mode[] = [
{ name: 'C', mime: 'text/x-csrc', mode: clike({}), ext: [ 'c', 'h', 'ino' ] },
{ name: 'C++', mime: 'text/x-c++src', mode: cpp(), ext: [ 'cpp', 'c++', 'cc', 'cxx', 'hpp', 'h++', 'hh', 'hxx' ], alias: [ 'cpp' ] },
{ name: 'C#', mime: 'text/x-csharp', mode: clike({}), ext: [ 'cs' ], alias: [ 'csharp', 'cs' ] },
{ name: 'CSS', mime: 'text/css', mode: css(), ext: [ 'css' ] },
{ name: 'CQL', mime: 'text/x-cassandra', mode: sql({ dialect: Cassandra }), ext: [ 'cql' ] },
{ name: 'Diff', mime: 'text/x-diff', mode: diff, ext: [ 'diff', 'patch' ] },
{ name: 'Dockerfile', mime: 'text/x-dockerfile', mode: dockerFile, file: /^Dockerfile$/ },
{ name: 'Git Markdown', mime: 'text/x-gfm', mode: markdown({ defaultCodeLanguage: markdownLanguage }), file: /^(readme|contributing|history|license).md$/i },
{ name: 'Golang', mime: 'text/x-go', mode: go, ext: [ 'go' ] },
{ name: 'HTML', mime: 'text/html', mode: html(), ext: [ 'html', 'htm', 'handlebars', 'hbs' ], alias: [ 'xhtml' ] },
{ name: 'HTTP', mime: 'message/http', mode: http },
{ name: 'JavaScript', mime: 'text/javascript', mimes: [ 'text/javascript', 'text/ecmascript', 'application/javascript', 'application/x-javascript', 'application/ecmascript' ], mode: javascript(), ext: [ 'js' ], alias: [ 'ecmascript', 'js', 'node' ] },
{ name: 'JSON', mime: 'application/json', mimes: [ 'application/json', 'application/x-json' ], mode: json(), ext: [ 'json', 'json5', 'map' ], alias: [ 'json5' ] },
{ name: 'Lua', mime: 'text/x-lua', mode: lua, ext: [ 'lua' ] },
{ name: 'Markdown', mime: 'text/x-markdown', mode: markdown({ defaultCodeLanguage: markdownLanguage }), ext: [ 'markdown', 'md', 'mkd' ] },
{ name: 'MariaDB', mime: 'text/x-mariadb', mode: sql({ dialect: MariaSQL }) },
{ name: 'MS SQL', mime: 'text/x-mssql', mode: sql({ dialect: MSSQL }) },
{ name: 'MySQL', mime: 'text/x-mysql', mode: sql({ dialect: MySQL }) },
{ name: 'Plain Text', mime: 'text/plain', mode: undefined, ext: [ 'txt', 'text', 'conf', 'def', 'list', 'log' ] },
{ name: 'PostgreSQL', mime: 'text/x-pgsql', mode: sql({ dialect: PostgreSQL }) },
{ name: 'Properties', mime: 'text/x-properties', mode: properties, ext: [ 'properties', 'ini', 'in' ], alias: [ 'ini', 'properties' ] },
{ name: 'Python', mime: 'text/x-python', mode: python, ext: [ 'BUILD', 'bzl', 'py', 'pyw' ], file: /^(BUCK|BUILD)$/ },
{ name: 'Ruby', mime: 'text/x-ruby', mode: ruby, ext: [ 'rb' ], alias: [ 'jruby', 'macruby', 'rake', 'rb', 'rbx' ] },
{ name: 'Rust', mime: 'text/x-rustsrc', mode: rust(), ext: [ 'rs' ] },
{ name: 'Sass', mime: 'text/x-sass', mode: css(), ext: [ 'sass' ] },
{ name: 'SCSS', mime: 'text/x-scss', mode: css(), ext: [ 'scss' ] },
{ name: 'Shell', mime: 'text/x-sh', mimes: [ 'text/x-sh', 'application/x-sh' ], mode: shell, ext: [ 'sh', 'ksh', 'bash' ], alias: [ 'bash', 'sh', 'zsh' ], file: /^PKGBUILD$/ },
{ name: 'SQL', mime: 'text/x-sql', mode: sql({ dialect: StandardSQL }), ext: [ 'sql' ] },
{ name: 'SQLite', mime: 'text/x-sqlite', mode: sql({ dialect: SQLite }) },
{ name: 'TOML', mime: 'text/x-toml', mode: toml, ext: [ 'toml' ] },
{ name: 'TypeScript', mime: 'application/typescript', mode: typescriptLanguage, ext: [ 'ts' ], alias: [ 'ts' ] },
{ name: 'XML', mime: 'application/xml', mimes: [ 'application/xml', 'text/xml' ], mode: xml(), ext: [ 'xml', 'xsl', 'xsd', 'svg' ], alias: [ 'rss', 'wsdl', 'xsd' ] },
{ name: 'YAML', mime: 'text/x-yaml', mimes: [ 'text/x-yaml', 'text/yaml' ], mode: yaml, ext: [ 'yaml', 'yml' ], alias: [ 'yml' ] },
];
export const modeToExtension = (m: EditorMode): Extension => {
if (m instanceof LanguageSupport) {
return m;
}
if (m instanceof LRLanguage) {
return m;
}
return StreamLanguage.define(m);
};
const findModeByFilename = (filename: string): Mode => {
for (let i = 0; i < modes.length; i++) {
const info = modes[i];
if (info.file && info.file.test(filename)) {
return info;
}
}
const dot = filename.lastIndexOf('.');
const ext = dot > -1 && filename.substring(dot + 1, filename.length);
if (ext) {
for (let i = 0; i < modes.length; i++) {
const info = modes[i];
if (info.ext) {
for (let j = 0; j < info.ext.length; j++) {
if (info.ext[j] === ext) {
return info;
}
}
}
}
}
const plainText = modes.find(m => m.mime === 'text/plain');
if (plainText === undefined) {
throw new Error('failed to find \'text/plain\' mode');
}
return plainText;
};
const findLanguageExtensionByMode = (mode: Mode): Extension => {
if (mode.mode === undefined) {
return [];
}
return modeToExtension(mode.mode);
};
const defaultExtensions: Extension = [
ayuMirage,
lineNumbers(),
highlightActiveLineGutter(),
highlightSpecialChars(),
history(),
foldGutter(),
drawSelection(),
EditorState.allowMultipleSelections.of(true),
indentOnInput(),
defaultHighlightStyle.fallback,
bracketMatching(),
closeBrackets(),
autocompletion(),
rectangularSelection(),
highlightActiveLine(),
highlightSelectionMatches(),
keymap.of([
...closeBracketsKeymap,
...defaultKeymap,
...searchKeymap,
...historyKeymap,
...foldKeymap,
...commentKeymap,
...completionKeymap,
...lintKeymap,
indentWithTab,
]),
EditorState.tabSize.of(4),
// This is gonna piss people off, but that isn't my problem.
indentUnit.of('\t'),
];
const EditorContainer = styled.div<{ overrides?: TwStyle }>`
//min-height: 12rem;
${tw`relative`};
& > div {
${props => props.overrides};
&.cm-focused {
outline: none;
}
}
`;
export interface Props {
className?: string;
style?: React.CSSProperties;
overrides?: TwStyle;
initialContent?: string;
extensions?: Extension[];
mode?: EditorMode;
filename?: string;
onModeChanged?: (mode: Mode) => void;
fetchContent?: (callback: () => Promise<string>) => void;
onContentSaved?: () => void;
}
export default ({ className, style, overrides, initialContent, extensions, mode, filename, onModeChanged, fetchContent, onContentSaved }: Props) => {
const [ languageConfig ] = useState<Compartment>(new Compartment());
const [ keybinds ] = useState<Compartment>(new Compartment());
const [ view, setView ] = useState<EditorView>();
const createEditorState = () => {
return EditorState.create({
doc: initialContent,
extensions: [
...defaultExtensions,
...(extensions !== undefined ? extensions : []),
languageConfig.of(mode !== undefined ? modeToExtension(mode) : findLanguageExtensionByMode(findModeByFilename(filename || ''))),
keybinds.of([]),
],
});
};
const ref = useCallback((node) => {
if (!node) {
return;
}
const view = new EditorView({
state: createEditorState(),
parent: node,
});
setView(view);
}, []);
// This useEffect is required to send the proper mode back to the parent element
// due to the initial language being set with EditorState#create, rather than in
// an useEffect like this one, or one watching `filename`.
useEffect(() => {
if (onModeChanged === undefined) {
return;
}
onModeChanged(findModeByFilename(filename || ''));
}, []);
useEffect(() => {
if (view === undefined) {
return;
}
if (mode === undefined) {
return;
}
view.dispatch({
effects: languageConfig.reconfigure(modeToExtension(mode)),
});
}, [ mode ]);
useEffect(() => {
if (view === undefined) {
return;
}
if (filename === undefined) {
return;
}
const mode = findModeByFilename(filename || '');
view.dispatch({
effects: languageConfig.reconfigure(findLanguageExtensionByMode(mode)),
});
if (onModeChanged !== undefined) {
onModeChanged(mode);
}
}, [ filename ]);
useEffect(() => {
if (view === undefined) {
return;
}
// We could dispatch a view update to replace the content, but this would keep the edit history,
// and previously would duplicate the content of the editor.
view.setState(createEditorState());
}, [ initialContent ]);
useEffect(() => {
if (fetchContent === undefined) {
return;
}
if (!view) {
fetchContent(() => Promise.reject(new Error('no editor session has been configured')));
return;
}
if (onContentSaved !== undefined) {
view.dispatch({
effects: keybinds.reconfigure(keymap.of([
{
key: 'Mod-s',
run: () => {
onContentSaved();
return true;
},
},
])),
});
}
fetchContent(() => Promise.resolve(view.state.doc.toString()));
}, [ view, fetchContent, onContentSaved ]);
return (
<EditorContainer className={className} style={style} overrides={overrides} ref={ref}/>
);
};

View file

@ -1,6 +1,8 @@
import { forwardRef } from 'react'; import { forwardRef } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { ButtonProps, Options } from '@/components/elements/button/types';
import type { ButtonProps } from '@/components/elements/button/types';
import { Options } from '@/components/elements/button/types';
import styles from './style.module.css'; import styles from './style.module.css';
const Button = forwardRef<HTMLButtonElement, ButtonProps>( const Button = forwardRef<HTMLButtonElement, ButtonProps>(

View file

@ -1,15 +1,25 @@
import { forwardRef } from 'react';
import * as React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import type { ComponentProps } from 'react';
import { forwardRef } from 'react';
import styles from './styles.module.css'; import styles from './styles.module.css';
type Props = Omit<React.ComponentProps<'input'>, 'type'>; type Props = Omit<ComponentProps<'input'>, 'type'> & {
indeterminate?: boolean;
};
export default forwardRef<HTMLInputElement, Props>(({ className, ...props }, ref) => ( export default forwardRef<HTMLInputElement, Props>(({ className, indeterminate, ...props }, ref) => (
<input <input
ref={ref} ref={ref}
type={'checkbox'} type={'checkbox'}
className={classNames('form-input', styles.checkbox_input, className)} className={classNames(
'form-checkbox',
{
[styles.checkbox]: true,
[styles.indeterminate]: indeterminate,
},
className,
)}
{...props} {...props}
/> />
)); ));

View file

@ -48,9 +48,9 @@ import Sidebar from '@/components/admin/Sidebar';
import UsersContainer from '@/components/admin/users/UsersContainer'; import UsersContainer from '@/components/admin/users/UsersContainer';
function AdminRouter() { function AdminRouter() {
// const email = useStoreState((state: State<ApplicationStore>) => state.user.data!.email); const email = useStoreState((state: ApplicationStore) => state.user.data!.email);
// const roleName = useStoreState((state: State<ApplicationStore>) => state.user.data!.roleName); const roleName = useStoreState((state: ApplicationStore) => state.user.data!.roleName);
// const avatarURL = useStoreState((state: State<ApplicationStore>) => state.user.data!.avatarURL); const avatarURL = useStoreState((state: ApplicationStore) => state.user.data!.avatarURL);
const applicationName = useStoreState((state: ApplicationStore) => state.settings.data!.name); const applicationName = useStoreState((state: ApplicationStore) => state.settings.data!.name);
// const [collapsed, setCollapsed] = useUserPersistedState('admin_sidebar_collapsed', false); // const [collapsed, setCollapsed] = useUserPersistedState('admin_sidebar_collapsed', false);
@ -71,7 +71,7 @@ function AdminRouter() {
</div> </div>
<Sidebar.Wrapper> <Sidebar.Wrapper>
<Sidebar.Section>Administration</Sidebar.Section> <Sidebar.Section>Administration</Sidebar.Section>
<NavLink to="/admin"> <NavLink to="/admin" end>
<OfficeBuildingIcon /> <OfficeBuildingIcon />
<span>Overview</span> <span>Overview</span>
</NavLink> </NavLink>
@ -118,27 +118,27 @@ function AdminRouter() {
<ReplyIcon /> <ReplyIcon />
<span>Return</span> <span>Return</span>
</NavLink> </NavLink>
{/*<Sidebar.User>*/} <Sidebar.User>
{/* {avatarURL && (*/} {avatarURL && (
{/* <img*/} <img
{/* src={`${avatarURL}?s=64`}*/} src={`${avatarURL}?s=64`}
{/* alt="Profile Picture"*/} alt="Profile Picture"
{/* css={tw`h-10 w-10 rounded-full select-none`}*/} css={tw`h-10 w-10 rounded-full select-none`}
{/* />*/} />
{/* )}*/} )}
{/* <div css={tw`flex flex-col ml-3`}>*/} <div css={tw`flex flex-col ml-3`}>
{/* <span*/} <span
{/* css={tw`font-sans font-normal text-sm text-neutral-50 whitespace-nowrap leading-tight select-none`}*/} css={tw`font-sans font-normal text-sm text-neutral-50 whitespace-nowrap leading-tight select-none`}
{/* >*/} >
{/* {email}*/} {email}
{/* </span>*/} </span>
{/* <span*/} <span
{/* css={tw`font-header font-normal text-xs text-neutral-300 whitespace-nowrap leading-tight select-none`}*/} css={tw`font-header font-normal text-xs text-neutral-300 whitespace-nowrap leading-tight select-none`}
{/* >*/} >
{/* {roleName}*/} {roleName}
{/* </span>*/} </span>
{/* </div>*/} </div>
{/*</Sidebar.User>*/} </Sidebar.User>
</Sidebar> </Sidebar>
<div css={tw`flex-1 overflow-x-hidden px-6 pt-6 lg:px-10 lg:pt-8 xl:px-16 xl:pt-12`}> <div css={tw`flex-1 overflow-x-hidden px-6 pt-6 lg:px-10 lg:pt-8 xl:px-16 xl:pt-12`}>

View file

@ -64,7 +64,7 @@ function ServerRouter() {
error ? ( error ? (
<ServerError message={error} /> <ServerError message={error} />
) : ( ) : (
<Spinner size={'large'} centered /> <Spinner size="large" centered />
) )
) : ( ) : (
<> <>
@ -75,21 +75,24 @@ function ServerRouter() {
.map(route => .map(route =>
route.permission ? ( route.permission ? (
<Can key={route.path} action={route.permission} matchAny> <Can key={route.path} action={route.permission} matchAny>
<NavLink to={`/server/${id}/${route.path ?? ''}`} end> <NavLink to={`/server/${id}/${route.path ?? ''}`.replace(/\/$/, '')} end>
{route.name} {route.name}
</NavLink> </NavLink>
</Can> </Can>
) : ( ) : (
<NavLink key={route.path} to={`/server/${id}/${route.path ?? ''}`} end> <NavLink
key={route.path}
to={`/server/${id}/${route.path ?? ''}`.replace(/\/$/, '')}
end
>
{route.name} {route.name}
</NavLink> </NavLink>
), ),
)} )}
{rootAdmin && ( {rootAdmin && (
// eslint-disable-next-line react/jsx-no-target-blank <NavLink to={`/admin/servers/${serverId}`}>
<a href={`/admin/servers/view/${serverId}`} target="_blank">
<FontAwesomeIcon icon={faExternalLinkAlt} /> <FontAwesomeIcon icon={faExternalLinkAlt} />
</a> </NavLink>
)} )}
</div> </div>
</SubNavigation> </SubNavigation>

View file

@ -8,6 +8,8 @@ export interface UserData {
language: string; language: string;
rootAdmin: boolean; rootAdmin: boolean;
useTotp: boolean; useTotp: boolean;
avatarURL: string;
roleName: string;
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
} }