Skip to content

Commit

Permalink
Merge pull request #79 from Lodestone-Team/76-user-management-pages
Browse files Browse the repository at this point in the history
  • Loading branch information
Ynng authored Jan 5, 2023
2 parents 0fe1c76 + 59de01e commit 20ae6b1
Show file tree
Hide file tree
Showing 37 changed files with 1,777 additions and 421 deletions.
13 changes: 10 additions & 3 deletions pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ import NotFound from 'pages/notfound';
import FirstTime from 'pages/login/FirstTime';
import RequireCore from 'utils/router/RequireCore';
import RequireToken from 'utils/router/RequireToken';
import { InstanceViewLayout } from 'components/DashboardLayout/InstanceViewLayout';
import { SettingsLayout } from 'components/DashboardLayout/SettingsLayout';
import { toast } from 'react-toastify';

const queryClient = new QueryClient({
Expand Down Expand Up @@ -160,6 +162,7 @@ export default function App() {
});
// TODO: clear ongoing notifications as well
} else {
// can't use useDecodedToken here because LodestoneContext is not available yet
try {
const decoded = jwt.decode(token, { complete: true });
if (!decoded) throw new Error('Invalid token');
Expand Down Expand Up @@ -253,9 +256,13 @@ export default function App() {
</RequireCore>
}
>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<SettingsPage />} />
<Route path="/" element={<Home />} />
<Route element={<InstanceViewLayout />}>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/" element={<Home />} />
</Route>
<Route element={<SettingsLayout />}>
<Route path="/settings" element={<SettingsPage />} />
</Route>
</Route>
<Route path="*" element={<NotFound />} />
</Routes>
Expand Down
17 changes: 10 additions & 7 deletions src/components/Atoms/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import BoringAvatar, { AvatarProps } from 'boring-avatars';
import clsx from 'clsx';

const Avatar = ({
name,
Expand All @@ -8,13 +9,15 @@ const Avatar = ({
...props
}: AvatarProps) => {
return (
<BoringAvatar
name={name}
size={size}
variant={variant}
colors={colors}
{...props}
/>
<div className={clsx(`w-[${size}px] h-[${size}px]`)}>
<BoringAvatar
name={name}
size={size}
variant={variant}
colors={colors}
{...props}
/>
</div>
);
};

Expand Down
17 changes: 5 additions & 12 deletions src/components/Atoms/Checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,9 @@ export default function Checkbox({
disabled?: boolean;
className?: string;
}) {
const [isChecked, setIsChecked] = useState(checked);

// set isChecked to checked when checked changes
useEffect(() => {
setIsChecked(checked);
}, [checked]);

const handleClick = (e: React.MouseEvent) => {
if (disabled) return;
setIsChecked(!isChecked);
onChange(!isChecked);
onChange(!checked);
};

return (
Expand All @@ -47,18 +39,19 @@ export default function Checkbox({
disabled && 'text-gray-500',
!disabled && [
'cursor-pointer hover:bg-gray-faded/30',
isChecked && 'text-gray-300 hover:text-gray-300',
!isChecked && 'text-gray-400 hover:text-gray-300',
checked && 'text-gray-300 hover:text-gray-300',
!checked && 'text-gray-400 hover:text-gray-300',
]
)}
onClick={handleClick}
>
<FontAwesomeIcon icon={isChecked ? faCheckSquare : faSquare} />
<FontAwesomeIcon icon={checked ? faCheckSquare : faSquare} />
</div>
{label && (
<label
onClick={handleClick}
className={clsx(
'truncate',
disabled && 'text-gray-500',
!disabled && 'text-gray-300 hover:text-gray-300'
)}
Expand Down
10 changes: 6 additions & 4 deletions src/components/Atoms/Config/SelectBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import { catchAsyncToString } from 'utils/util';

/**
* A self controlled dropdown meant to represent a single value of a config
*
*
* It is NOT meant to be used as a form input
*
*
* See SelectField for that
*/
export default function SelectBox({
Expand All @@ -27,6 +27,7 @@ export default function SelectBox({
descriptionFunc,
actionIcon,
actionIconClick,
optimistic = true, // if true, the dropdown will change immediately and go into loading state, and will change back if onChange throws an error
}: {
label: string;
value?: string;
Expand All @@ -41,6 +42,7 @@ export default function SelectBox({
descriptionFunc?: (arg: string) => React.ReactNode;
actionIcon?: IconDefinition;
actionIconClick?: () => void;
optimistic?: boolean;
}) {
const [value, setValue] = useState(initialValue || 'Select...');
const [isLoading, setIsLoading] = useState<boolean>(false);
Expand All @@ -52,7 +54,7 @@ export default function SelectBox({
}, [initialValue]);

const onChange = async (newValue: string) => {
setValue(newValue);
if (optimistic) setValue(newValue);
setIsLoading(true);
const submitError = await catchAsyncToString(onChangeProp(newValue));
setError(submitError);
Expand Down Expand Up @@ -158,7 +160,7 @@ export default function SelectBox({
value={option}
className={clsx(
'relative cursor-pointer select-none py-2 pl-3 pr-4 text-gray-300',
'border-t border-gray-faded/30 last:border-b ui-active:border-y ui-active:border-white/50 ui-active:mb-[-1px] ui-active:z-50 ui-active:last:mb-0',
'border-t border-gray-faded/30 last:border-b ui-active:z-50 ui-active:mb-[-1px] ui-active:border-y ui-active:border-white/50 ui-active:last:mb-0',
'ui-selected:font-medium ui-not-selected:font-normal',
'ui-selected:ui-active:bg-gray-600 ui-not-selected:ui-active:bg-gray-800',
'ui-selected:ui-not-active:bg-gray-700 ui-not-selected:ui-not-active:bg-gray-850'
Expand Down
29 changes: 10 additions & 19 deletions src/components/Atoms/Config/ToggleBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import { useEffect, useState } from 'react';
import BeatLoader from 'react-spinners/BeatLoader';
import { catchAsyncToString } from 'utils/util';
import { Switch } from '@headlessui/react';
import { Toggle } from '../Toggle';

/**
* A self controlled toggle component meant to represent a single value of a config
*
*
* It is NOT meant to be used as a form input
*
*
* See ToggleField for that
*/
export default function ToggleBox({
Expand All @@ -21,6 +22,7 @@ export default function ToggleBox({
isLoading: isLoadingProp = false,
description,
descriptionFunc,
optimistic = true, // if true, the toggle will change immediately and go into loading state, and will change back if onChange throws an error
}: {
label: string;
value: boolean;
Expand All @@ -32,6 +34,7 @@ export default function ToggleBox({
onChange: (arg: boolean) => Promise<void>;
description?: React.ReactNode;
descriptionFunc?: (arg: boolean) => React.ReactNode;
optimistic?: boolean;
}) {
const [value, setValue] = useState(initialValue);
const [isLoading, setIsLoading] = useState<boolean>(false);
Expand All @@ -43,7 +46,8 @@ export default function ToggleBox({
}, [initialValue]);

const onChange = async (newValue: boolean) => {
setValue(newValue);
if(optimistic)
setValue(newValue);
setIsLoading(true);
const submitError = await catchAsyncToString(onChangeProp(newValue));
setError(submitError);
Expand Down Expand Up @@ -97,24 +101,11 @@ export default function ToggleBox({
</div>
<div className="relative flex w-5/12 shrink-0 flex-row items-center justify-end gap-4">
{status}
<Switch
checked={value}
<Toggle
value={value}
onChange={onChange}
className={`${
disabled
? 'bg-gray-faded/30'
: value
? 'bg-green-enabled/50'
: 'bg-white/50'
} relative inline-flex h-6 w-11 items-center rounded-full outline-0 enabled:focus-visible:ring-4 enabled:focus-visible:ring-blue-faded/50`}
disabled={disabled || isLoading}
>
<span
className={`${value ? 'translate-x-6' : 'translate-x-1'} ${
disabled || isLoading ? 'bg-gray-faded/40' : 'bg-white'
} inline-block h-4 w-4 transform rounded-full`}
/>
</Switch>
/>
</div>
</div>
);
Expand Down
16 changes: 13 additions & 3 deletions src/components/Atoms/ConfirmDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Dialog, Transition } from '@headlessui/react';
import { Fragment, ReactNode } from 'react';
import { Fragment, ReactNode, useEffect, useState } from 'react';
import Button from './Button';

export interface DialogProps {
Expand All @@ -25,6 +25,11 @@ export default function ConfirmDialog({
isOpen,
zIndex = 10,
}: DialogProps) {
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
setIsLoading(false);
}, [isOpen]);

return (
<Transition
appear
Expand Down Expand Up @@ -57,7 +62,7 @@ export default function ConfirmDialog({
</Dialog.Title>
<Dialog.Description
as="p"
className="text-medium tracking-medium text-gray-300"
className="overflow-hidden text-medium tracking-medium text-gray-300"
>
{children}
</Dialog.Description>
Expand All @@ -66,13 +71,18 @@ export default function ConfirmDialog({
label={closeButtonText || 'Cancel'}
className={onConfirm ? 'w-fit' : 'grow'}
onClick={onClose}
disabled={isLoading}
/>
{onConfirm && (
<Button
label={confirmButtonText || 'Confirm'}
className="grow"
color={type === 'danger' ? 'danger' : 'info'}
onClick={onConfirm}
loading={isLoading}
onClick={() => {
setIsLoading(true);
onConfirm();
}}
/>
)}
</div>
Expand Down
18 changes: 18 additions & 0 deletions src/components/Atoms/HorizontalLine.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import clsx from 'clsx';
import React from 'react';
export const HorizontalLine = ({
thicknessClass = 'h-1', //set thickness using height classes
colorClass = 'bg-white/50', //set color using background-color classes
className,
}: {
thicknessClass?: string;
colorClass?: string;
className?: string;
}) => {
return (
<div
className={clsx(thicknessClass, colorClass, className)}
style={{ width: '100%' }}
/>
);
};
66 changes: 66 additions & 0 deletions src/components/Atoms/MultiSelectGrid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import clsx from 'clsx';
import Checkbox from './Checkbox';

export type MultiSelectGridProps<T extends string | object> = {
className?: string;
disabled?: boolean;
options: T[];
isLoading?: boolean;
selectedOptions: T[];
onChange: (selectedOptions: T[]) => void;
optionLabel?: (option: T) => string;
};

/**
* A grid of checkboxes meant to be used as a controlled component
*/
export default function MultiSelectGrid<T extends string | object>(
props: MultiSelectGridProps<T>
) {
const {
className,
disabled,
options,
selectedOptions,
onChange,
optionLabel = (option) => {
let output = '';
if (typeof option === 'string') {
output = option;
} else {
output = JSON.stringify(option);
}
console.log('optionLabel', option, output);
return output;
},
} = props;

const onCheckboxChange = (option: T, checked: boolean) => {
if (checked) {
onChange([...selectedOptions, option]);
} else {
onChange(selectedOptions.filter((o) => o !== option));
}
};

return (
<div
className={clsx(
'grid grid-cols-2 gap-4 @lg:grid-cols-4',
disabled ? 'bg-gray-850' : 'bg-gray-800',
className
)}
>
{options.map((option) => (
<Checkbox
key={optionLabel(option)}
label={optionLabel(option)}
checked={selectedOptions.includes(option)}
onChange={(checked) => onCheckboxChange(option, checked)}
disabled={disabled}
className="pr-4"
/>
))}
</div>
);
}
37 changes: 37 additions & 0 deletions src/components/Atoms/Toggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Switch } from '@headlessui/react';
import clsx from 'clsx';

export const Toggle = ({
value,
onChange,
disabled = false,
}: {
value: boolean;
onChange: (value: boolean) => void;
disabled?: boolean;
}) => {
return (
<Switch
checked={value}
onChange={onChange}
className={clsx(
'relative inline-flex h-6 w-11 items-center rounded-full outline-0 enabled:focus-visible:ring-4 enabled:focus-visible:ring-blue-faded/50',
{
'bg-gray-faded/30': disabled,
'bg-green-enabled/50': value && !disabled,
'bg-white/50': !value && !disabled,
}
)}
disabled={disabled}
>
<span
className={clsx('inline-block h-4 w-4 transform rounded-full', {
'translate-x-6': value,
'translate-x-1': !value,
'bg-gray-faded/40': disabled,
'bg-white': !disabled,
})}
/>
</Switch>
);
};
Loading

0 comments on commit 20ae6b1

Please sign in to comment.