From bc59ffbf37c7bcf828d3bbb7f27eec41d38e02e8 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 20 Feb 2022 19:10:58 -0500 Subject: [PATCH 1/8] Initial concept at a user table listing using Tailwind JIT & Headless UI --- package.json | 1 + .../admin/users/UsersContainerV2.tsx | 104 ++++++++++++++++++ resources/scripts/routers/AdminRouter.tsx | 4 +- yarn.lock | 11 ++ 4 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 resources/scripts/components/admin/users/UsersContainerV2.tsx diff --git a/package.json b/package.json index ea937ca0e..ef66f6e0a 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@fortawesome/free-regular-svg-icons": "^5.15.4", "@fortawesome/free-solid-svg-icons": "^5.15.4", "@fortawesome/react-fontawesome": "^0.1.16", + "@headlessui/react": "^1.5.0", "@heroicons/react": "^1.0.5", "@hot-loader/react-dom": "^16.14.0", "axios": "^0.21.4", diff --git a/resources/scripts/components/admin/users/UsersContainerV2.tsx b/resources/scripts/components/admin/users/UsersContainerV2.tsx new file mode 100644 index 000000000..91d0fe5d0 --- /dev/null +++ b/resources/scripts/components/admin/users/UsersContainerV2.tsx @@ -0,0 +1,104 @@ +import React, { useEffect, useState } from 'react'; +import http from '@/api/http'; +import { User } from '@/api/admin/user'; +import { AdminTransformers } from '@/api/admin/transformers'; +import { Menu } from '@headlessui/react'; +import { ChevronDownIcon, LockClosedIcon } from '@heroicons/react/solid'; + +const UsersContainerV2 = () => { + const [ users, setUsers ] = useState([]); + useEffect(() => { + document.title = 'Admin | Users'; + }, []); + + useEffect(() => { + http.get('/api/application/users') + .then(({ data }) => { + setUsers(data.data.map(AdminTransformers.toUser)); + }) + .catch(console.error); + }, []); + + return ( +
+ + + + + + + + {users.map(user => ( + + + + + + ))} + +
+ Email +
+
+ +
+
+
+
+ {'User +
+
+

+ {user.email} +

+

+ {user.uuid} +

+
+
+
+ + + Options + + + +
+ + {() => ( + + + Reset Password + + )} + + + {() => ( + Delete + )} + + + + Resend Invite + + +
+
+
+
+
+ ); +}; + +export default UsersContainerV2; diff --git a/resources/scripts/routers/AdminRouter.tsx b/resources/scripts/routers/AdminRouter.tsx index 0adc0f1a2..d006c0551 100644 --- a/resources/scripts/routers/AdminRouter.tsx +++ b/resources/scripts/routers/AdminRouter.tsx @@ -15,7 +15,6 @@ import LocationEditContainer from '@/components/admin/locations/LocationEditCont import ServersContainer from '@/components/admin/servers/ServersContainer'; import NewServerContainer from '@/components/admin/servers/NewServerContainer'; import ServerRouter from '@/components/admin/servers/ServerRouter'; -import UsersContainer from '@/components/admin/users/UsersContainer'; import NewUserContainer from '@/components/admin/users/NewUserContainer'; import UserRouter from '@/components/admin/users/UserRouter'; import RolesContainer from '@/components/admin/roles/RolesContainer'; @@ -46,6 +45,7 @@ import { import CollapsedIcon from '@/assets/images/pterodactyl.svg'; import Sidebar from '@/components/admin/Sidebar'; import useUserPersistedState from '@/plugins/useUserPersistedState'; +import UsersContainerV2 from '@/components/admin/users/UsersContainerV2'; const AdminRouter = ({ location, match }: RouteComponentProps) => { const email = useStoreState((state: State) => state.user.data!.email); @@ -132,7 +132,7 @@ const AdminRouter = ({ location, match }: RouteComponentProps) => { - + diff --git a/yarn.lock b/yarn.lock index 3445720e6..c5b13323a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2474,6 +2474,16 @@ __metadata: languageName: node linkType: hard +"@headlessui/react@npm:^1.5.0": + version: 1.5.0 + resolution: "@headlessui/react@npm:1.5.0" + peerDependencies: + react: ^16 || ^17 || ^18 + react-dom: ^16 || ^17 || ^18 + checksum: e3373dfb73936950d659a87718fb9fdedc599b344b30cb0d8b1ef0a2ec13d6f405c653fa51284236788658840ece82c6a8538a5a5931e595274e9590b2020079 + languageName: node + linkType: hard + "@heroicons/react@npm:^1.0.5": version: 1.0.5 resolution: "@heroicons/react@npm:1.0.5" @@ -10933,6 +10943,7 @@ fsevents@^1.2.7: "@fortawesome/free-regular-svg-icons": ^5.15.4 "@fortawesome/free-solid-svg-icons": ^5.15.4 "@fortawesome/react-fontawesome": ^0.1.16 + "@headlessui/react": ^1.5.0 "@heroicons/react": ^1.0.5 "@hot-loader/react-dom": ^16.14.0 "@tailwindcss/forms": ^0.4.0 From f4119df0aa3b223030e98caf315210b2a4d687b4 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 26 Feb 2022 15:13:13 -0500 Subject: [PATCH 2/8] Add basic dropdown styling using headless ui --- package.json | 1 + .../admin/users/UsersContainerV2.tsx | 53 +++++----------- .../components/elements/dropdown/Dropdown.tsx | 62 +++++++++++++++++++ .../elements/dropdown/DropdownButton.tsx | 24 +++++++ .../elements/dropdown/DropdownItem.tsx | 33 ++++++++++ .../components/elements/dropdown/index.ts | 2 + .../elements/dropdown/styles.module.css | 54 ++++++++++++++++ resources/scripts/globals.d.ts | 1 + webpack.config.js | 5 ++ yarn.lock | 8 +++ 10 files changed, 204 insertions(+), 39 deletions(-) create mode 100644 resources/scripts/components/elements/dropdown/Dropdown.tsx create mode 100644 resources/scripts/components/elements/dropdown/DropdownButton.tsx create mode 100644 resources/scripts/components/elements/dropdown/DropdownItem.tsx create mode 100644 resources/scripts/components/elements/dropdown/index.ts create mode 100644 resources/scripts/components/elements/dropdown/styles.module.css diff --git a/package.json b/package.json index ef66f6e0a..bdeea5211 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "@hot-loader/react-dom": "^16.14.0", "axios": "^0.21.4", "chart.js": "^2.9.4", + "classnames": "^2.3.1", "date-fns": "^2.25.0", "debounce": "^1.2.1", "deepmerge": "^4.2.2", diff --git a/resources/scripts/components/admin/users/UsersContainerV2.tsx b/resources/scripts/components/admin/users/UsersContainerV2.tsx index 91d0fe5d0..4010d301f 100644 --- a/resources/scripts/components/admin/users/UsersContainerV2.tsx +++ b/resources/scripts/components/admin/users/UsersContainerV2.tsx @@ -2,8 +2,8 @@ import React, { useEffect, useState } from 'react'; import http from '@/api/http'; import { User } from '@/api/admin/user'; import { AdminTransformers } from '@/api/admin/transformers'; -import { Menu } from '@headlessui/react'; -import { ChevronDownIcon, LockClosedIcon } from '@heroicons/react/solid'; +import { Dropdown } from '@/components/elements/dropdown'; +import { DotsVerticalIcon, LockClosedIcon, PaperAirplaneIcon, PencilIcon, TrashIcon } from '@heroicons/react/solid'; const UsersContainerV2 = () => { const [ users, setUsers ] = useState([]); @@ -20,8 +20,8 @@ const UsersContainerV2 = () => { }, []); return ( -
- +
+
))} diff --git a/resources/scripts/components/elements/dropdown/Dropdown.tsx b/resources/scripts/components/elements/dropdown/Dropdown.tsx new file mode 100644 index 000000000..8e43ccb7f --- /dev/null +++ b/resources/scripts/components/elements/dropdown/Dropdown.tsx @@ -0,0 +1,62 @@ +import React, { ElementType, forwardRef, useMemo } from 'react'; +import { Menu, Transition } from '@headlessui/react'; +import styles from './styles.module.css'; +import classNames from 'classnames'; +import DropdownItem from '@/components/elements/dropdown/DropdownItem'; +import DropdownButton from '@/components/elements/dropdown/DropdownButton'; + +interface Props { + as?: ElementType; + children: React.ReactNode; +} + +const DropdownGap = ({ invisible }: { invisible?: boolean }) => ( +
+); + +type TypedChild = (React.ReactChild | React.ReactFragment | React.ReactPortal) & { + type?: JSX.Element; +} + +const Dropdown = forwardRef(({ as, children }, ref) => { + const [ Button, items ] = useMemo(() => { + const list = React.Children.toArray(children) as unknown as TypedChild[]; + + return [ + list.filter(child => child.type === DropdownButton), + list.filter(child => child.type !== DropdownButton), + ]; + }, [ children ]); + + if (!Button) { + throw new Error('Cannot mount component without a child .'); + } + + return ( + + {Button} + + +
+ {items} +
+
+
+
+ ); +}); + +const _Dropdown = Object.assign(Dropdown, { + Button: DropdownButton, + Item: DropdownItem, + Gap: DropdownGap, +}); + +export { _Dropdown as default }; diff --git a/resources/scripts/components/elements/dropdown/DropdownButton.tsx b/resources/scripts/components/elements/dropdown/DropdownButton.tsx new file mode 100644 index 000000000..87e3af288 --- /dev/null +++ b/resources/scripts/components/elements/dropdown/DropdownButton.tsx @@ -0,0 +1,24 @@ +import classNames from 'classnames'; +import styles from '@/components/elements/dropdown/styles.module.css'; +import { ChevronDownIcon } from '@heroicons/react/solid'; +import { Menu } from '@headlessui/react'; +import React from 'react'; + +interface Props { + className?: string; + animate?: boolean; + children: React.ReactNode; +} + +export default ({ className, animate = true, children }: Props) => ( + + {typeof children === 'string' ? + <> + {children} + + + : + children + } + +); diff --git a/resources/scripts/components/elements/dropdown/DropdownItem.tsx b/resources/scripts/components/elements/dropdown/DropdownItem.tsx new file mode 100644 index 000000000..ca1f39173 --- /dev/null +++ b/resources/scripts/components/elements/dropdown/DropdownItem.tsx @@ -0,0 +1,33 @@ +import React, { forwardRef } from 'react'; +import { Menu } from '@headlessui/react'; +import styles from './styles.module.css'; +import classNames from 'classnames'; + +interface Props { + children: React.ReactNode | ((opts: { active: boolean; disabled: boolean }) => JSX.Element); + danger?: boolean; + disabled?: boolean; + className?: string; + icon?: JSX.Element; + onClick?: (e: React.MouseEvent) => void; +} + +const DropdownItem = forwardRef(({ disabled, danger, className, onClick, children, icon: IconComponent }, ref) => { + return ( + + {(args) => ( + + {IconComponent} + {typeof children === 'function' ? children(args) : children} + + )} + + ); +}); + +export default DropdownItem; diff --git a/resources/scripts/components/elements/dropdown/index.ts b/resources/scripts/components/elements/dropdown/index.ts new file mode 100644 index 000000000..97067f43e --- /dev/null +++ b/resources/scripts/components/elements/dropdown/index.ts @@ -0,0 +1,2 @@ +export { default as Dropdown } from './Dropdown'; +export * as styles from './styles.module.css'; diff --git a/resources/scripts/components/elements/dropdown/styles.module.css b/resources/scripts/components/elements/dropdown/styles.module.css new file mode 100644 index 000000000..f4025c90c --- /dev/null +++ b/resources/scripts/components/elements/dropdown/styles.module.css @@ -0,0 +1,54 @@ +.menu { + @apply relative inline-block text-left; + + & .button { + @apply inline-flex justify-center items-center w-full py-2 text-neutral-100 rounded-md; + @apply transition-all duration-100; + + &:hover, &[aria-expanded="true"] { + @apply bg-neutral-600 text-white; + } + + &:focus, &:focus-within, &:active { + @apply ring-2 ring-opacity-50 ring-neutral-300 text-white; + } + + & svg { + @apply w-5 h-5 transition-transform duration-75; + } + + &[aria-expanded="true"] svg[data-animated="true"] { + @apply rotate-180; + } + } + + & .items_container { + @apply absolute right-0 mt-2 origin-top-right bg-neutral-900 rounded z-10; + } +} + +.menu_item { + @apply flex items-center rounded w-full px-2 py-2; + + & svg { + @apply w-4 h-4 mr-4 text-neutral-300; + } + + &:hover, &:focus, &:focus-within { + @apply bg-primary-600 text-primary-50; + + & svg { + @apply text-primary-50; + } + } + + &.danger { + &:hover, &:focus, &:focus-within { + @apply bg-red-500 text-red-50; + + & svg { + @apply text-red-50; + } + } + } +} diff --git a/resources/scripts/globals.d.ts b/resources/scripts/globals.d.ts index bb38b7421..b08b72719 100644 --- a/resources/scripts/globals.d.ts +++ b/resources/scripts/globals.d.ts @@ -1,3 +1,4 @@ declare module '*.jpg'; declare module '*.png'; declare module '*.svg'; +declare module '*.css'; diff --git a/webpack.config.js b/webpack.config.js index a7d7effb2..22cd3c18d 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -36,6 +36,11 @@ module.exports = { { loader: 'css-loader', options: { + modules: { + auto: true, + localIdentName: isProduction ? '[name]_[hash:base64:8]' : '[path][name]__[local]', + localIdentContext: path.join(__dirname, "resources/scripts/components"), + }, sourceMap: !isProduction, importLoaders: 1, }, diff --git a/yarn.lock b/yarn.lock index c5b13323a..d79ca6f68 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4650,6 +4650,13 @@ __metadata: languageName: node linkType: hard +"classnames@npm:^2.3.1": + version: 2.3.1 + resolution: "classnames@npm:2.3.1" + checksum: 14db8889d56c267a591f08b0834989fe542d47fac659af5a539e110cc4266694e8de86e4e3bbd271157dbd831361310a8293e0167141e80b0f03a0f175c80960 + languageName: node + linkType: hard + "clean-set@npm:^1.1.1": version: 1.1.2 resolution: "clean-set@npm:1.1.2" @@ -10976,6 +10983,7 @@ fsevents@^1.2.7: babel-plugin-styled-components: ^2.0.3 browserslist: ^4.17.6 chart.js: ^2.9.4 + classnames: ^2.3.1 cross-env: ^7.0.3 css-loader: ^5.2.7 date-fns: ^2.25.0 From 0bab962337db6fc6e3c87db39cb84e29d406e142 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 26 Feb 2022 17:05:30 -0500 Subject: [PATCH 3/8] Add underlying design component for a dialog --- .../admin/users/UsersContainerV2.tsx | 42 +++++-- .../components/elements/button/Button.tsx | 36 ++++++ .../elements/button/button.module.css | 30 +++++ .../components/elements/button/index.ts | 2 + .../components/elements/dialog/Dialog.tsx | 116 ++++++++++++++++++ .../elements/dialog/dialog.module.css | 7 ++ .../components/elements/dialog/index.ts | 2 + .../components/elements/dropdown/Dropdown.tsx | 2 +- .../elements/dropdown/DropdownButton.tsx | 2 +- .../elements/dropdown/DropdownItem.tsx | 2 +- ...{styles.module.css => dropdown.module.css} | 0 .../components/elements/dropdown/index.ts | 2 +- tailwind.config.js | 8 +- 13 files changed, 233 insertions(+), 18 deletions(-) create mode 100644 resources/scripts/components/elements/button/Button.tsx create mode 100644 resources/scripts/components/elements/button/button.module.css create mode 100644 resources/scripts/components/elements/button/index.ts create mode 100644 resources/scripts/components/elements/dialog/Dialog.tsx create mode 100644 resources/scripts/components/elements/dialog/dialog.module.css create mode 100644 resources/scripts/components/elements/dialog/index.ts rename resources/scripts/components/elements/dropdown/{styles.module.css => dropdown.module.css} (100%) diff --git a/resources/scripts/components/admin/users/UsersContainerV2.tsx b/resources/scripts/components/admin/users/UsersContainerV2.tsx index 4010d301f..537b26c44 100644 --- a/resources/scripts/components/admin/users/UsersContainerV2.tsx +++ b/resources/scripts/components/admin/users/UsersContainerV2.tsx @@ -3,7 +3,16 @@ import http from '@/api/http'; import { User } from '@/api/admin/user'; import { AdminTransformers } from '@/api/admin/transformers'; import { Dropdown } from '@/components/elements/dropdown'; -import { DotsVerticalIcon, LockClosedIcon, PaperAirplaneIcon, PencilIcon, TrashIcon } from '@heroicons/react/solid'; +import { + DotsVerticalIcon, + LockClosedIcon, + PaperAirplaneIcon, + PencilIcon, + PlusIcon, + TrashIcon, +} from '@heroicons/react/solid'; +import { Button } from '@/components/elements/button/index'; +import { Dialog } from '@/components/elements/dialog'; const UsersContainerV2 = () => { const [ users, setUsers ] = useState([]); @@ -11,6 +20,8 @@ const UsersContainerV2 = () => { document.title = 'Admin | Users'; }, []); + const [ visible, setVisible ] = useState(false); + useEffect(() => { http.get('/api/application/users') .then(({ data }) => { @@ -20,8 +31,21 @@ const UsersContainerV2 = () => { }, []); return ( -
-
@@ -57,41 +57,16 @@ const UsersContainerV2 = () => { - - - Options - - - -
- - {() => ( - - - Reset Password - - )} - - - {() => ( - Delete - )} - - - - Resend Invite - - -
-
-
+ + + + + }>Edit + }>Reset Password + }>Suspend + + } danger>Delete Account +
+
+
+ +
+ setVisible(false)}> + + This account will be permanently deleted. + + setVisible(false)}>Cancel + Delete + + +
diff --git a/resources/scripts/components/elements/button/Button.tsx b/resources/scripts/components/elements/button/Button.tsx new file mode 100644 index 000000000..f3238beac --- /dev/null +++ b/resources/scripts/components/elements/button/Button.tsx @@ -0,0 +1,36 @@ +import React, { forwardRef } from 'react'; +import classNames from 'classnames'; +import styles from './button.module.css'; + +export type ButtonProps = JSX.IntrinsicElements['button'] & { + square?: boolean; + small?: boolean; +} + +const Button = forwardRef( + ({ children, square, small, className, ...rest }, ref) => { + return ( + + ); + }, +); + +const TextButton = forwardRef(({ className, ...props }, ref) => ( + // @ts-expect-error +
@@ -59,13 +83,13 @@ const UsersContainerV2 = () => { - + - }>Edit - }>Reset Password - }>Suspend - - } danger>Delete Account + }>Edit + }>Reset Password + }>Suspend + + } onClick={() => setVisible(true)} danger>Delete Account
@@ -58,7 +59,7 @@ const UsersContainerV2 = () => { @@ -59,7 +65,7 @@ const UsersContainerV2 = () => { + diff --git a/resources/scripts/components/elements/button/button.module.css b/resources/scripts/components/elements/button/button.module.css index 3931526fc..728e14155 100644 --- a/resources/scripts/components/elements/button/button.module.css +++ b/resources/scripts/components/elements/button/button.module.css @@ -22,7 +22,7 @@ } .text { - @apply bg-transparent focus:ring-neutral-300 focus:ring-opacity-50 hover:bg-neutral-600 active:bg-neutral-500; + @apply bg-transparent focus:ring-neutral-300 focus:ring-opacity-50 hover:bg-neutral-500 active:bg-neutral-500; } .danger { diff --git a/resources/scripts/components/elements/dialog/Dialog.tsx b/resources/scripts/components/elements/dialog/Dialog.tsx index 4f2e4d988..ae07719f3 100644 --- a/resources/scripts/components/elements/dialog/Dialog.tsx +++ b/resources/scripts/components/elements/dialog/Dialog.tsx @@ -78,12 +78,12 @@ const Dialog = ({ visible, title, onDismissed, children }: Props) => { leaveFrom={'opacity-100 scale-100'} leaveTo={'opacity-0 scale-95'} > -
+
{icon &&
{icon}
}
{title && - + {title} } @@ -92,11 +92,7 @@ const Dialog = ({ visible, title, onDismissed, children }: Props) => {
- {buttons && -
- {buttons} -
- } + {buttons &&
{buttons}
} {/* Keep this below the other buttons so that it isn't the default focus if they're present. */}
onDismissed()} className={'hover:rotate-90'}> diff --git a/resources/scripts/components/elements/dialog/dialog.module.css b/resources/scripts/components/elements/dialog/dialog.module.css index 9bc9ff670..94959ee77 100644 --- a/resources/scripts/components/elements/dialog/dialog.module.css +++ b/resources/scripts/components/elements/dialog/dialog.module.css @@ -3,5 +3,18 @@ } .overlay { - @apply fixed inset-0 bg-neutral-900 opacity-75; + @apply fixed inset-0 bg-gray-900 opacity-50; +} + +.container { + @apply relative bg-gray-600 rounded max-w-xl w-full mx-auto shadow-lg; + @apply ring-4 ring-gray-800 ring-opacity-80; + + & .title { + @apply font-header text-xl font-medium mb-2 text-white pr-4; + } + + & > .button_bar { + @apply px-6 py-3 bg-gray-700 flex items-center justify-end space-x-3 rounded-b; + } } diff --git a/resources/scripts/components/elements/dropdown/DropdownItem.tsx b/resources/scripts/components/elements/dropdown/DropdownItem.tsx index 41f6720af..e431336e4 100644 --- a/resources/scripts/components/elements/dropdown/DropdownItem.tsx +++ b/resources/scripts/components/elements/dropdown/DropdownItem.tsx @@ -12,18 +12,28 @@ interface Props { onClick?: (e: React.MouseEvent) => void; } -const DropdownItem = forwardRef(({ disabled, danger, className, onClick, children, icon: IconComponent }, ref) => { +const DropdownItem = forwardRef(({ + disabled, + danger, + className, + onClick, + children, + icon: IconComponent, +}, ref) => { return ( - {(args) => ( + {({ disabled, active }) => ( {IconComponent} - {typeof children === 'function' ? children(args) : children} + {typeof children === 'function' ? children({ disabled, active }) : children} )} diff --git a/resources/scripts/components/elements/dropdown/dropdown.module.css b/resources/scripts/components/elements/dropdown/dropdown.module.css index 73ed043e4..0b98e3428 100644 --- a/resources/scripts/components/elements/dropdown/dropdown.module.css +++ b/resources/scripts/components/elements/dropdown/dropdown.module.css @@ -34,7 +34,7 @@ @apply w-4 h-4 mr-4 text-neutral-300; } - &:hover, &:focus, &:focus-within { + &:hover, &:focus { @apply bg-blue-500 text-blue-50; & svg { @@ -43,7 +43,7 @@ } &.danger { - &:hover, &:focus, &:focus-within { + &:hover, &:focus { @apply bg-red-500 text-red-50; & svg { @@ -51,4 +51,8 @@ } } } + + &.disabled { + @apply cursor-not-allowed hover:bg-neutral-800 opacity-30 focus:bg-transparent focus:hover:bg-neutral-800; + } } diff --git a/tailwind.config.js b/tailwind.config.js index e53495649..d826fcc33 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -11,9 +11,10 @@ module.exports = { }, colors: { black: '#131a20', - // Deprecated, prefer the use of "blue" directly here. + // Deprecated, prefer "blue"... primary: colors.blue, - neutral: colors.neutral, + // Deprecate, prefer "gray"... + neutral: colors.gray, cyan: colors.cyan, }, fontSize: { From 308a7f3a90199f59bd860c9ca5b4fec6c8b3a941 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 26 Feb 2022 21:51:40 -0500 Subject: [PATCH 6/8] Add input field --- .../components/admin/users/UsersContainerV2.tsx | 16 ++++++++++++---- .../components/elements/inputs/InputField.tsx | 11 +++++++++++ .../scripts/components/elements/inputs/index.ts | 1 + .../components/elements/inputs/inputs.module.css | 9 +++++++++ 4 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 resources/scripts/components/elements/inputs/InputField.tsx diff --git a/resources/scripts/components/admin/users/UsersContainerV2.tsx b/resources/scripts/components/admin/users/UsersContainerV2.tsx index d0b1118af..21c85ae4d 100644 --- a/resources/scripts/components/admin/users/UsersContainerV2.tsx +++ b/resources/scripts/components/admin/users/UsersContainerV2.tsx @@ -14,7 +14,7 @@ import { } from '@heroicons/react/solid'; import { Button } from '@/components/elements/button/index'; import { Dialog } from '@/components/elements/dialog'; -import { Checkbox } from '@/components/elements/inputs'; +import { Checkbox, InputField } from '@/components/elements/inputs'; const UsersContainerV2 = () => { const [ users, setUsers ] = useState([]); @@ -44,13 +44,21 @@ const UsersContainerV2 = () => { This account will be permanently deleted. setVisible(false)} - >Cancel + > + Cancel - Delete + Delete +
+
+ +
+
+ +
+
- +
diff --git a/resources/scripts/components/elements/button/button.module.css b/resources/scripts/components/elements/button/button.module.css index 717da280e..3931526fc 100644 --- a/resources/scripts/components/elements/button/button.module.css +++ b/resources/scripts/components/elements/button/button.module.css @@ -1,14 +1,14 @@ .button { @apply px-4 py-2 inline-flex items-center justify-center; - @apply bg-primary-500 rounded text-base font-semibold text-primary-50 transition-all duration-100; - @apply hover:bg-primary-600 active:bg-primary-600; + @apply bg-blue-600 rounded text-base font-semibold text-blue-50 transition-all duration-100; + @apply hover:bg-blue-500 active:bg-blue-500; &.square { @apply p-2; } &:focus { - @apply ring-[3px] ring-opacity-75 ring-primary-300; + @apply ring-[3px] ring-blue-500 ring-offset-2 ring-offset-neutral-700; } /* Sizing Controls */ @@ -26,5 +26,5 @@ } .danger { - @apply bg-red-500 hover:bg-red-600 active:bg-red-600 focus:ring-red-400 text-red-50; + @apply bg-red-600 hover:bg-red-500 active:bg-red-500 focus:ring-red-500 text-red-50; } diff --git a/resources/scripts/components/elements/dropdown/dropdown.module.css b/resources/scripts/components/elements/dropdown/dropdown.module.css index f4025c90c..73ed043e4 100644 --- a/resources/scripts/components/elements/dropdown/dropdown.module.css +++ b/resources/scripts/components/elements/dropdown/dropdown.module.css @@ -35,10 +35,10 @@ } &:hover, &:focus, &:focus-within { - @apply bg-primary-600 text-primary-50; + @apply bg-blue-500 text-blue-50; & svg { - @apply text-primary-50; + @apply text-blue-50; } } diff --git a/resources/scripts/components/elements/inputs/Checkbox.tsx b/resources/scripts/components/elements/inputs/Checkbox.tsx new file mode 100644 index 000000000..7e5d1e3c7 --- /dev/null +++ b/resources/scripts/components/elements/inputs/Checkbox.tsx @@ -0,0 +1,13 @@ +import React, { forwardRef } from 'react'; +import styles from './inputs.module.css'; +import classNames from 'classnames'; + +type Props = Omit, 'type'> + +export default forwardRef(({ className, ...props }, ref) => ( + +)); diff --git a/resources/scripts/components/elements/inputs/index.ts b/resources/scripts/components/elements/inputs/index.ts new file mode 100644 index 000000000..11e194edf --- /dev/null +++ b/resources/scripts/components/elements/inputs/index.ts @@ -0,0 +1,2 @@ +export { default as Checkbox } from './Checkbox'; +export { default as styles } from './inputs.module.css'; diff --git a/resources/scripts/components/elements/inputs/inputs.module.css b/resources/scripts/components/elements/inputs/inputs.module.css new file mode 100644 index 000000000..7808a8343 --- /dev/null +++ b/resources/scripts/components/elements/inputs/inputs.module.css @@ -0,0 +1,7 @@ +.checkbox { + @apply w-4 h-4 rounded-sm border-neutral-500 bg-neutral-600 text-primary-500; + + &:focus, &:active { + @apply ring-2 ring-primary-500 ring-offset-2 ring-offset-neutral-700; + } +} diff --git a/tailwind.config.js b/tailwind.config.js index 497be5282..e53495649 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -11,30 +11,9 @@ module.exports = { }, colors: { black: '#131a20', - primary: { - 50: '#e6f6ff', - 100: '#b8e2ff', - 200: '#7ac3fa', - 300: '#49a4f3', - 400: '#2487eb', - 500: '#0967d3', - 600: '#0550b3', - 700: '#0345a0', - 800: '#01337e', - 900: '#002057', - }, - neutral: { - 50: '#f5f7fa', - 100: '#e5e8eb', - 200: '#cad1d8', - 300: '#9aa5b1', - 400: '#7b8793', - 500: '#606d7b', - 600: '#515f6c', - 700: '#3f4d5a', - 800: '#33404d', - 900: '#1f2933', - }, + // Deprecated, prefer the use of "blue" directly here. + primary: colors.blue, + neutral: colors.neutral, cyan: colors.cyan, }, fontSize: { @@ -49,6 +28,8 @@ module.exports = { }, }, plugins: [ - require('@tailwindcss/forms'), + require('@tailwindcss/forms')({ + strategy: 'class', + }), ] }; From ae522f1585507a369de0ebbdfbd8762243ad2c6c Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 26 Feb 2022 21:26:53 -0500 Subject: [PATCH 5/8] More color adjustments --- .../admin/users/UsersContainerV2.tsx | 32 +++++++++++++++---- .../elements/button/button.module.css | 2 +- .../components/elements/dialog/Dialog.tsx | 10 ++---- .../elements/dialog/dialog.module.css | 15 ++++++++- .../elements/dropdown/DropdownItem.tsx | 18 ++++++++--- .../elements/dropdown/dropdown.module.css | 8 +++-- tailwind.config.js | 5 +-- 7 files changed, 66 insertions(+), 24 deletions(-) diff --git a/resources/scripts/components/admin/users/UsersContainerV2.tsx b/resources/scripts/components/admin/users/UsersContainerV2.tsx index cb1635379..d0b1118af 100644 --- a/resources/scripts/components/admin/users/UsersContainerV2.tsx +++ b/resources/scripts/components/admin/users/UsersContainerV2.tsx @@ -4,11 +4,12 @@ import { User } from '@/api/admin/user'; import { AdminTransformers } from '@/api/admin/transformers'; import { Dropdown } from '@/components/elements/dropdown'; import { + BanIcon, DotsVerticalIcon, - LockClosedIcon, - PaperAirplaneIcon, + LockOpenIcon, PencilIcon, PlusIcon, + SupportIcon, TrashIcon, } from '@heroicons/react/solid'; import { Button } from '@/components/elements/button/index'; @@ -42,7 +43,11 @@ const UsersContainerV2 = () => { This account will be permanently deleted. - setVisible(false)}>Cancel + setVisible(false)} + >Cancel + Delete @@ -52,6 +57,7 @@ const UsersContainerV2 = () => { Email +
- +
@@ -81,16 +87,28 @@ const UsersContainerV2 = () => { + {user.isUsingTwoFactor && + + 2-FA Enabled + + } + }>Edit - }>Reset Password - }>Suspend + }>Reset Password + } disabled={!user.isUsingTwoFactor}> + Disable 2-FA + + }>Suspend - } onClick={() => setVisible(true)} danger>Delete Account + } onClick={() => setVisible(true)} danger>Delete + Account +
diff --git a/resources/scripts/components/elements/inputs/InputField.tsx b/resources/scripts/components/elements/inputs/InputField.tsx new file mode 100644 index 000000000..fe90679f7 --- /dev/null +++ b/resources/scripts/components/elements/inputs/InputField.tsx @@ -0,0 +1,11 @@ +import React, { forwardRef } from 'react'; +import classNames from 'classnames'; +import styles from './inputs.module.css'; + +export default forwardRef>(({ className, ...props }, ref) => ( + +)); diff --git a/resources/scripts/components/elements/inputs/index.ts b/resources/scripts/components/elements/inputs/index.ts index 11e194edf..cd6d8964f 100644 --- a/resources/scripts/components/elements/inputs/index.ts +++ b/resources/scripts/components/elements/inputs/index.ts @@ -1,2 +1,3 @@ export { default as Checkbox } from './Checkbox'; +export { default as InputField } from './InputField'; export { default as styles } from './inputs.module.css'; diff --git a/resources/scripts/components/elements/inputs/inputs.module.css b/resources/scripts/components/elements/inputs/inputs.module.css index 7808a8343..c32a23fb8 100644 --- a/resources/scripts/components/elements/inputs/inputs.module.css +++ b/resources/scripts/components/elements/inputs/inputs.module.css @@ -5,3 +5,12 @@ @apply ring-2 ring-primary-500 ring-offset-2 ring-offset-neutral-700; } } + +.text_input { + @apply transition-all duration-75; + @apply bg-neutral-800 border-neutral-600 rounded px-4 py-2 outline-none; + + &:focus { + @apply border-blue-600 ring-2 ring-blue-500; + } +} From e02f4b84330c9232421beb87539a4b33ef93f734 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 26 Feb 2022 22:06:47 -0500 Subject: [PATCH 7/8] Raw styling needed for mass actions bar --- .../components/admin/users/UsersContainerV2.tsx | 16 +++++++++++++++- .../components/elements/inputs/Checkbox.tsx | 12 +++++++++--- .../components/elements/inputs/inputs.module.css | 5 +++++ 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/resources/scripts/components/admin/users/UsersContainerV2.tsx b/resources/scripts/components/admin/users/UsersContainerV2.tsx index 21c85ae4d..96f57feca 100644 --- a/resources/scripts/components/admin/users/UsersContainerV2.tsx +++ b/resources/scripts/components/admin/users/UsersContainerV2.tsx @@ -51,13 +51,27 @@ const UsersContainerV2 = () => { Delete -
+
+
+
+ +
+ + + + + + + + + +
diff --git a/resources/scripts/components/elements/inputs/Checkbox.tsx b/resources/scripts/components/elements/inputs/Checkbox.tsx index 7e5d1e3c7..2c09c0367 100644 --- a/resources/scripts/components/elements/inputs/Checkbox.tsx +++ b/resources/scripts/components/elements/inputs/Checkbox.tsx @@ -2,12 +2,18 @@ import React, { forwardRef } from 'react'; import styles from './inputs.module.css'; import classNames from 'classnames'; -type Props = Omit, 'type'> +type Props = Omit, 'type'> & { + indeterminate?: boolean; +} -export default forwardRef(({ className, ...props }, ref) => ( +export default forwardRef(({ className, indeterminate, ...props }, ref) => ( )); diff --git a/resources/scripts/components/elements/inputs/inputs.module.css b/resources/scripts/components/elements/inputs/inputs.module.css index c32a23fb8..80295c7a8 100644 --- a/resources/scripts/components/elements/inputs/inputs.module.css +++ b/resources/scripts/components/elements/inputs/inputs.module.css @@ -4,6 +4,11 @@ &:focus, &:active { @apply ring-2 ring-primary-500 ring-offset-2 ring-offset-neutral-700; } + + &.indeterminate:checked { + @apply text-primary-500/50 border border-primary-500; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='white'%3E%3Cpath fill-rule='evenodd' d='M5 10a1 1 0 011-1h8a1 1 0 110 2H6a1 1 0 01-1-1z' clip-rule='evenodd' /%3E%3C/svg%3E"); + } } .text_input { From cd5c2bc5fd7a14440de4eb10b4249915c1cdb850 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 27 Feb 2022 10:50:25 -0500 Subject: [PATCH 8/8] Fix incompatible signatures after update --- .../Allocations/StoreAllocationRequest.php | 12 ++++++++++-- .../Api/Application/Nodes/StoreNodeRequest.php | 13 ++++++++++--- .../Databases/StoreServerDatabaseRequest.php | 12 ++++++++++-- .../Api/Application/Servers/StoreServerRequest.php | 12 ++++++++++-- .../UpdateServerBuildConfigurationRequest.php | 11 +++++++++-- .../Servers/UpdateServerDetailsRequest.php | 10 ++++++++-- .../Api/Application/Servers/UpdateServerRequest.php | 13 ++++++++++--- app/Models/Node.php | 7 ++++++- 8 files changed, 73 insertions(+), 17 deletions(-) diff --git a/app/Http/Requests/Api/Application/Allocations/StoreAllocationRequest.php b/app/Http/Requests/Api/Application/Allocations/StoreAllocationRequest.php index dd97e1332..b0472b1fc 100644 --- a/app/Http/Requests/Api/Application/Allocations/StoreAllocationRequest.php +++ b/app/Http/Requests/Api/Application/Allocations/StoreAllocationRequest.php @@ -2,6 +2,7 @@ namespace Pterodactyl\Http\Requests\Api\Application\Allocations; +use Illuminate\Support\Arr; use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; class StoreAllocationRequest extends ApplicationApiRequest @@ -16,14 +17,21 @@ class StoreAllocationRequest extends ApplicationApiRequest ]; } - public function validated(): array + /** + * @param string|null $key + * @param string|array|null $default + * @return mixed + */ + public function validated($key = null, $default = null) { $data = parent::validated(); - return [ + $response = [ 'allocation_ip' => $data['ip'], 'allocation_ports' => $data['ports'], 'allocation_alias' => $data['alias'] ?? null, ]; + + return is_null($key) ? $response : Arr::get($response, $key, $default); } } diff --git a/app/Http/Requests/Api/Application/Nodes/StoreNodeRequest.php b/app/Http/Requests/Api/Application/Nodes/StoreNodeRequest.php index cef2169c7..65459091d 100644 --- a/app/Http/Requests/Api/Application/Nodes/StoreNodeRequest.php +++ b/app/Http/Requests/Api/Application/Nodes/StoreNodeRequest.php @@ -2,6 +2,7 @@ namespace Pterodactyl\Http\Requests\Api\Application\Nodes; +use Illuminate\Support\Arr; use Pterodactyl\Models\Node; use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; @@ -58,12 +59,18 @@ class StoreNodeRequest extends ApplicationApiRequest * Change the formatting of some data keys in the validated response data * to match what the application expects in the services. * - * @return array + * @param string|null $key + * @param string|array|null $default + * @return mixed */ - public function validated() + public function validated($key = null, $default = null) { $response = parent::validated(); - $response['daemon_base'] = $response['daemon_base'] ?? (new Node())->getAttribute('daemon_base'); + $response['daemon_base'] = $response['daemon_base'] ?? Node::DEFAULT_DAEMON_BASE; + + if (!is_null($key)) { + return Arr::get($response, $key, $default); + } return $response; } diff --git a/app/Http/Requests/Api/Application/Servers/Databases/StoreServerDatabaseRequest.php b/app/Http/Requests/Api/Application/Servers/Databases/StoreServerDatabaseRequest.php index f8549075a..4954176b8 100644 --- a/app/Http/Requests/Api/Application/Servers/Databases/StoreServerDatabaseRequest.php +++ b/app/Http/Requests/Api/Application/Servers/Databases/StoreServerDatabaseRequest.php @@ -2,6 +2,7 @@ namespace Pterodactyl\Http\Requests\Api\Application\Servers\Databases; +use Illuminate\Support\Arr; use Webmozart\Assert\Assert; use Pterodactyl\Models\Server; use Illuminate\Validation\Rule; @@ -30,13 +31,20 @@ class StoreServerDatabaseRequest extends ApplicationApiRequest ]; } - public function validated(): array + /** + * @param string|null $key + * @param string|array|null $default + * @return mixed + */ + public function validated($key = null, $default = null) { - return [ + $data = [ 'database' => $this->input('database'), 'remote' => $this->input('remote'), 'database_host_id' => $this->input('host'), ]; + + return is_null($key) ? $data : Arr::get($data, $key, $default); } public function attributes(): array diff --git a/app/Http/Requests/Api/Application/Servers/StoreServerRequest.php b/app/Http/Requests/Api/Application/Servers/StoreServerRequest.php index 452909abe..984895761 100644 --- a/app/Http/Requests/Api/Application/Servers/StoreServerRequest.php +++ b/app/Http/Requests/Api/Application/Servers/StoreServerRequest.php @@ -2,6 +2,7 @@ namespace Pterodactyl\Http\Requests\Api\Application\Servers; +use Illuminate\Support\Arr; use Pterodactyl\Models\Server; use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; @@ -43,11 +44,16 @@ class StoreServerRequest extends ApplicationApiRequest ]; } - public function validated(): array + /** + * @param string|null $key + * @param string|array|null $default + * @return array + */ + public function validated($key = null, $default = null) { $data = parent::validated(); - return [ + $response = [ 'external_id' => array_get($data, 'external_id'), 'name' => array_get($data, 'name'), 'description' => array_get($data, 'description'), @@ -76,5 +82,7 @@ class StoreServerRequest extends ApplicationApiRequest 'skip_scripts' => array_get($data, 'skip_scripts'), 'start_on_completion' => array_get($data, 'start_on_completion', false), ]; + + return is_null($key) ? $response : Arr::get($response, $key, $default); } } diff --git a/app/Http/Requests/Api/Application/Servers/UpdateServerBuildConfigurationRequest.php b/app/Http/Requests/Api/Application/Servers/UpdateServerBuildConfigurationRequest.php index 7045c1e08..28d2e3e39 100644 --- a/app/Http/Requests/Api/Application/Servers/UpdateServerBuildConfigurationRequest.php +++ b/app/Http/Requests/Api/Application/Servers/UpdateServerBuildConfigurationRequest.php @@ -2,6 +2,7 @@ namespace Pterodactyl\Http\Requests\Api\Application\Servers; +use Illuminate\Support\Arr; use Pterodactyl\Models\Server; use Illuminate\Support\Collection; @@ -52,9 +53,11 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest /** * Convert the allocation field into the expected format for the service handler. * - * @return array + * @param string|null $key + * @param string|array|null $default + * @return mixed */ - public function validated() + public function validated($key = null, $default = null) { $data = parent::validated(); @@ -73,6 +76,10 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest unset($data['limits']); } + if (!is_null($key)) { + return Arr::get($data, $key, $default); + } + return $data; } diff --git a/app/Http/Requests/Api/Application/Servers/UpdateServerDetailsRequest.php b/app/Http/Requests/Api/Application/Servers/UpdateServerDetailsRequest.php index 226f69d18..a4551edcd 100644 --- a/app/Http/Requests/Api/Application/Servers/UpdateServerDetailsRequest.php +++ b/app/Http/Requests/Api/Application/Servers/UpdateServerDetailsRequest.php @@ -2,6 +2,7 @@ namespace Pterodactyl\Http\Requests\Api\Application\Servers; +use Illuminate\Support\Arr; use Pterodactyl\Models\Server; class UpdateServerDetailsRequest extends ServerWriteRequest @@ -24,15 +25,20 @@ class UpdateServerDetailsRequest extends ServerWriteRequest /** * Convert the posted data into the correct format that is expected * by the application. + * + * @param string|null $key + * @param string|array|null $default */ - public function validated(): array + public function validated($key = null, $default = null) { - return [ + $data = [ 'external_id' => $this->input('external_id'), 'name' => $this->input('name'), 'owner_id' => $this->input('user'), 'description' => $this->input('description'), ]; + + return is_null($key) ? $data : Arr::get($data, $key, $default); } /** diff --git a/app/Http/Requests/Api/Application/Servers/UpdateServerRequest.php b/app/Http/Requests/Api/Application/Servers/UpdateServerRequest.php index 9b254d047..d07cffd03 100644 --- a/app/Http/Requests/Api/Application/Servers/UpdateServerRequest.php +++ b/app/Http/Requests/Api/Application/Servers/UpdateServerRequest.php @@ -2,6 +2,7 @@ namespace Pterodactyl\Http\Requests\Api\Application\Servers; +use Illuminate\Support\Arr; use Pterodactyl\Models\Server; use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; @@ -39,11 +40,15 @@ class UpdateServerRequest extends ApplicationApiRequest ]; } - public function validated(): array + /** + * @param string|null $key + * @param string|array|null $default + * @return mixed + */ + public function validated($key = null, $default = null) { $data = parent::validated(); - - return [ + $response = [ 'external_id' => array_get($data, 'external_id'), 'name' => array_get($data, 'name'), 'description' => array_get($data, 'description'), @@ -65,5 +70,7 @@ class UpdateServerRequest extends ApplicationApiRequest 'add_allocations' => array_get($data, 'add_allocations'), 'remove_allocations' => array_get($data, 'remove_allocations'), ]; + + return is_null($key) ? $response : Arr::get($response, $key, $default); } } diff --git a/app/Models/Node.php b/app/Models/Node.php index 51af2697c..740140898 100644 --- a/app/Models/Node.php +++ b/app/Models/Node.php @@ -18,6 +18,11 @@ class Node extends Model */ public const RESOURCE_NAME = 'node'; + /** + * The default location of server files on the Wings instance. + */ + final public const DEFAULT_DAEMON_BASE = '/var/lib/pterodactyl/volumes'; + public const DAEMON_TOKEN_ID_LENGTH = 16; public const DAEMON_TOKEN_LENGTH = 64; @@ -104,7 +109,7 @@ class Node extends Model 'behind_proxy' => false, 'memory_overallocate' => 0, 'disk_overallocate' => 0, - 'daemon_base' => '/var/lib/pterodactyl/volumes', + 'daemon_base' => self::DEFAULT_DAEMON_BASE, 'maintenance_mode' => false, ];