mirror of
https://github.com/pyrohost/pyrodactyl.git
synced 2026-04-06 04:01:58 +02:00
ui: Account Api/SSH Keys pages with new colros and changes
This commit is contained in:
@@ -6,16 +6,16 @@
|
||||
@theme {
|
||||
--color-ring: deepskyblue;
|
||||
|
||||
--color-brand-50: #fdf3ec;
|
||||
--color-brand-100: #fbe5d5;
|
||||
--color-brand-200: #f8ceaf;
|
||||
--color-brand-300: #f5b385;
|
||||
--color-brand-400: #f1995a;
|
||||
--color-brand-500: #ee8132;
|
||||
--color-brand-600: #d46312;
|
||||
--color-brand-700: #a04a0d;
|
||||
--color-brand-800: #6c3209;
|
||||
--color-brand-900: #341804;
|
||||
--color-brand-50: oklch(0.97 0.0143 57.59);
|
||||
--color-brand-100: oklch(0.9354 0.0323 58.39);
|
||||
--color-brand-200: oklch(0.8798 0.063 58.24);
|
||||
--color-brand-300: oklch(0.818 0.0981 55.75);
|
||||
--color-brand-400: oklch(0.7603 0.1318 54.85);
|
||||
--color-brand-500: oklch(0.7123 0.1602 52.23);
|
||||
--color-brand-600: oklch(0.6276 0.1636 48.48);
|
||||
--color-brand-700: oklch(0.5108 0.1316 48.74);
|
||||
--color-brand-800: oklch(0.3887 0.0962 49.79);
|
||||
--color-brand-900: oklch(0.2451 0.0546 53.64);
|
||||
|
||||
--color-cream-100: #fffdfa;
|
||||
--color-cream-200: #fff8f0;
|
||||
|
||||
31
resources/scripts/components/HeaderManger.tsx
Normal file
31
resources/scripts/components/HeaderManger.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import HeaderCentered from '@/components/dashboard/header/HeaderCentered';
|
||||
import { useHeader } from '@/contexts/HeaderContext';
|
||||
|
||||
interface headerProps {
|
||||
title: string;
|
||||
}
|
||||
|
||||
const ServerHeader = (props: headerProps) => {
|
||||
const { setHeaderActions, clearHeaderActions } = useHeader();
|
||||
|
||||
const statusSection = useMemo(
|
||||
() => (
|
||||
<HeaderCentered className='flex items-center gap-6'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<span>{props.title}</span>
|
||||
</div>
|
||||
</HeaderCentered>
|
||||
),
|
||||
[props.title],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setHeaderActions([statusSection]);
|
||||
return () => clearHeaderActions();
|
||||
}, [setHeaderActions, clearHeaderActions, statusSection]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default ServerHeader;
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Eye, EyeSlash, Key, Plus, TrashBin } from '@gravity-ui/icons';
|
||||
import { format } from 'date-fns';
|
||||
import { type Actions, useStoreActions } from 'easy-peasy';
|
||||
import { Field, Form, Formik, type FormikHelpers } from 'formik';
|
||||
import { type FormikHelpers } from 'formik';
|
||||
import { lazy, useEffect, useState } from 'react';
|
||||
import { object, string } from 'yup';
|
||||
import createApiKey from '@/api/account/createApiKey';
|
||||
import deleteApiKey from '@/api/account/deleteApiKey';
|
||||
import getApiKeys, { type ApiKey } from '@/api/account/getApiKeys';
|
||||
@@ -12,8 +11,6 @@ import ApiKeyModal from '@/components/dashboard/ApiKeyModal';
|
||||
import ActionButton from '@/components/elements/ActionButton';
|
||||
import Code from '@/components/elements/Code';
|
||||
import { Dialog } from '@/components/elements/dialog';
|
||||
import FormikFieldWrapper from '@/components/elements/FormikFieldWrapper';
|
||||
import Input from '@/components/elements/Input';
|
||||
import { MainPageHeader } from '@/components/elements/MainPageHeader';
|
||||
import PageContentBlock from '@/components/elements/PageContentBlock';
|
||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||
@@ -21,6 +18,9 @@ import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import { useFlashKey } from '@/plugins/useFlash';
|
||||
import type { ApplicationStore } from '@/state';
|
||||
|
||||
import ServerHeader from '../HeaderManger';
|
||||
|
||||
|
||||
const CreateApiKeyModal = lazy(() => import('./CreateApiKeyModal'));
|
||||
|
||||
interface CreateValues {
|
||||
@@ -83,9 +83,10 @@ const AccountApiContainer = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContentBlock title={'API Keys'}>
|
||||
<PageContentBlock title={'Api Key'}>
|
||||
<FlashMessageRender byKey='account:api-keys' />
|
||||
<ApiKeyModal visible={apiKey.length > 0} onModalDismissed={() => setApiKey('')} apiKey={apiKey} />
|
||||
<ServerHeader title='Api Keys' />
|
||||
|
||||
<CreateApiKeyModal
|
||||
open={showCreateModal}
|
||||
@@ -95,26 +96,21 @@ const AccountApiContainer = () => {
|
||||
|
||||
<div className='w-full h-full min-h-full flex-1 flex flex-col px-2 sm:px-0'>
|
||||
<div
|
||||
className='transform-gpu skeleton-anim-2 mb-3 sm:mb-4'
|
||||
className='transform-gpu skeleton-anim-2 mb-3 sm:mb-4 w-full'
|
||||
style={{
|
||||
animationDelay: '50ms',
|
||||
animationTimingFunction:
|
||||
'linear(0,0.01,0.04 1.6%,0.161 3.3%,0.816 9.4%,1.046,1.189 14.4%,1.231,1.254 17%,1.259,1.257 18.6%,1.236,1.194 22.3%,1.057 27%,0.999 29.4%,0.955 32.1%,0.942,0.935 34.9%,0.933,0.939 38.4%,1 47.3%,1.011,1.017 52.6%,1.016 56.4%,1 65.2%,0.996 70.2%,1.001 87.2%,1)',
|
||||
}}
|
||||
>
|
||||
<MainPageHeader
|
||||
title='API Keys'
|
||||
titleChildren={
|
||||
<ActionButton
|
||||
variant='primary'
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
className='flex items-center gap-2'
|
||||
>
|
||||
<Plus width={22} height={22} fill='currentColor' />
|
||||
Create API Key
|
||||
</ActionButton>
|
||||
}
|
||||
/>
|
||||
<ActionButton
|
||||
variant='secondary'
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
className='flex items-center gap-2'
|
||||
>
|
||||
<Plus width={22} height={22} fill='currentColor' />
|
||||
Create API Key
|
||||
</ActionButton>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -125,7 +121,7 @@ const AccountApiContainer = () => {
|
||||
'linear(0,0.01,0.04 1.6%,0.161 3.3%,0.816 9.4%,1.046,1.189 14.4%,1.231,1.254 17%,1.259,1.257 18.6%,1.236,1.194 22.3%,1.057 27%,0.999 29.4%,0.955 32.1%,0.942,0.935 34.9%,0.933,0.939 38.4%,1 47.3%,1.011,1.017 52.6%,1.016 56.4%,1 65.2%,0.996 70.2%,1.001 87.2%,1)',
|
||||
}}
|
||||
>
|
||||
<div className='bg-gradient-to-b from-[#ffffff08] to-[#ffffff05] border-[1px] border-[#ffffff12] rounded-xl p-4 sm:p-6 shadow-sm'>
|
||||
<div className='bg-mocha-500 border-[1px] border-[#ffffff12] hover:border-[#ffffff15] rounded-xl p-4 sm:p-6 shadow-sm'>
|
||||
<SpinnerOverlay visible={loading} />
|
||||
<Dialog.Confirm
|
||||
title={'Delete API Key'}
|
||||
@@ -139,7 +135,7 @@ const AccountApiContainer = () => {
|
||||
|
||||
{keys.length === 0 ? (
|
||||
<div className='text-center py-12'>
|
||||
<div className='w-16 h-16 mx-auto mb-4 rounded-full bg-[#ffffff11] flex items-center justify-center'>
|
||||
<div className='w-16 h-16 mx-auto mb-4 rounded-full bg-mocha-400 flex items-center justify-center'>
|
||||
<Key width={22} height={22} className='text-zinc-400' fill='currentColor' />
|
||||
</div>
|
||||
<h3 className='text-lg font-medium text-zinc-200 mb-2'>No API Keys</h3>
|
||||
@@ -161,7 +157,7 @@ const AccountApiContainer = () => {
|
||||
'linear(0,0.01,0.04 1.6%,0.161 3.3%,0.816 9.4%,1.046,1.189 14.4%,1.231,1.254 17%,1.259,1.257 18.6%,1.236,1.194 22.3%,1.057 27%,0.999 29.4%,0.955 32.1%,0.942,0.935 34.9%,0.933,0.939 38.4%,1 47.3%,1.011,1.017 52.6%,1.016 56.4%,1 65.2%,0.996 70.2%,1.001 87.2%,1)',
|
||||
}}
|
||||
>
|
||||
<div className='bg-[#ffffff05] border-[1px] border-[#ffffff08] rounded-lg p-4 hover:border-[#ffffff15] transition-all duration-150'>
|
||||
<div className='rounded-lg transition-all duration-150'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex-1 min-w-0'>
|
||||
<div className='flex items-center gap-3 mb-2'>
|
||||
@@ -178,7 +174,7 @@ const AccountApiContainer = () => {
|
||||
</span>
|
||||
<div className='flex items-center gap-2'>
|
||||
<span>Key:</span>
|
||||
<code className='font-mono px-2 py-1 bg-[#ffffff08] border border-[#ffffff08] rounded text-zinc-300'>
|
||||
<code className='font-mono px-2 py-1 bg-mocha-400 border border-mocha-200 rounded text-zinc-300'>
|
||||
{showKeys[key.identifier]
|
||||
? key.identifier
|
||||
: '••••••••••••••••'}
|
||||
|
||||
@@ -67,7 +67,7 @@ const DashboardContainer = () => {
|
||||
const searchSection = useMemo(
|
||||
() => (
|
||||
<HeaderCentered>
|
||||
<SearchSection className='max-w-128 xl:w-[30vw] hidden md:flex ' />
|
||||
<SearchSection className='max-w-240 xl:w-[30vw] hidden md:flex ' />
|
||||
</HeaderCentered>
|
||||
),
|
||||
[],
|
||||
@@ -103,8 +103,12 @@ const DashboardContainer = () => {
|
||||
() => (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size={'sm'} variant={'secondary'} className='px-1 pl-3 gap-1 rounded-full'>
|
||||
<div className='flex flex-row items-center gap-1'>
|
||||
<Button
|
||||
size={'sm'}
|
||||
variant={'secondary'}
|
||||
className='px-1 pl-3 gap-1 rounded-full hover:cursor-pointer'
|
||||
>
|
||||
<div className='flex flex-row items-center gap-1 '>
|
||||
<div className='flex flex-row items-center gap-1.5'>
|
||||
<HugeiconsIcon size={16} strokeWidth={2} icon={FilterIcon} className='size-4' />
|
||||
Filter
|
||||
@@ -113,7 +117,7 @@ const DashboardContainer = () => {
|
||||
</div>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className='flex flex-col gap-1 z-99999' sideOffset={8}>
|
||||
<DropdownMenuContent className='flex flex-col gap-1 z-99999 hover:cursor-pointer' sideOffset={8}>
|
||||
<DropdownMenuItem
|
||||
onSelect={() => setServerViewMode('owner')}
|
||||
className={serverViewMode === 'owner' ? 'bg-accent/20' : ''}
|
||||
@@ -185,13 +189,12 @@ const DashboardContainer = () => {
|
||||
{items.map((server, index) => (
|
||||
<div
|
||||
key={`${server.uuid}-${dashboardDisplayOption}`}
|
||||
className={`transform-gpu skeleton-anim-2 ${
|
||||
dashboardDisplayOption === 'grid'
|
||||
className={`transform-gpu skeleton-anim-2 ${dashboardDisplayOption === 'grid'
|
||||
? items.length === 1
|
||||
? 'w-[calc(50%-0.5rem)] max-lg:w-full'
|
||||
: 'w-[calc(50%-0.5rem)] max-lg:w-full'
|
||||
: 'mb-4'
|
||||
} max-lg:mb-4`}
|
||||
} max-lg:mb-4`}
|
||||
style={{
|
||||
animationDelay: `${index * 50 + 50}ms`,
|
||||
animationTimingFunction:
|
||||
@@ -218,8 +221,8 @@ const DashboardContainer = () => {
|
||||
{serverViewMode === 'admin-all'
|
||||
? 'There are no other servers to display.'
|
||||
: serverViewMode === 'all'
|
||||
? 'No Server Shared With your Account'
|
||||
: 'There are no servers associated with your account.'}
|
||||
? 'No Server Shared With your Account'
|
||||
: 'There are no servers associated with your account.'}
|
||||
</p>
|
||||
<h3 className='text-lg font-medium text-zinc-200 mb-2'>
|
||||
{serverViewMode === 'admin-all' ? 'No other servers found' : 'No servers found'}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Eye, EyeSlash, Key, Plus, TrashBin } from '@gravity-ui/icons';
|
||||
import { format } from 'date-fns';
|
||||
import { type Actions, useStoreActions } from 'easy-peasy';
|
||||
import { Field, Form, Formik, type FormikHelpers } from 'formik';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useState, useMemo } from 'react';
|
||||
import { object, string } from 'yup';
|
||||
import { createSSHKey, deleteSSHKey, useSSHKeys } from '@/api/account/ssh-keys';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
@@ -18,6 +18,8 @@ import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import { useFlashKey } from '@/plugins/useFlash';
|
||||
import type { ApplicationStore } from '@/state';
|
||||
|
||||
import ServerHeader from '@/components/HeaderManger';
|
||||
|
||||
interface CreateValues {
|
||||
name: string;
|
||||
publicKey: string;
|
||||
@@ -85,6 +87,7 @@ const AccountSSHContainer = () => {
|
||||
return (
|
||||
<PageContentBlock title={'SSH Keys'}>
|
||||
<FlashMessageRender byKey='account:ssh-keys' />
|
||||
<ServerHeader title='SSH Keys' />
|
||||
|
||||
{/* Create SSH Key Modal */}
|
||||
{showCreateModal && (
|
||||
@@ -145,19 +148,14 @@ const AccountSSHContainer = () => {
|
||||
'linear(0,0.01,0.04 1.6%,0.161 3.3%,0.816 9.4%,1.046,1.189 14.4%,1.231,1.254 17%,1.259,1.257 18.6%,1.236,1.194 22.3%,1.057 27%,0.999 29.4%,0.955 32.1%,0.942,0.935 34.9%,0.933,0.939 38.4%,1 47.3%,1.011,1.017 52.6%,1.016 56.4%,1 65.2%,0.996 70.2%,1.001 87.2%,1)',
|
||||
}}
|
||||
>
|
||||
<MainPageHeader
|
||||
title='SSH Keys'
|
||||
titleChildren={
|
||||
<ActionButton
|
||||
variant='primary'
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
className='flex items-center gap-2'
|
||||
>
|
||||
<Plus width={22} height={22} fill='currentColor' />
|
||||
Add SSH Key
|
||||
</ActionButton>
|
||||
}
|
||||
/>
|
||||
<ActionButton
|
||||
variant='secondary'
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
className='flex items-center gap-2'
|
||||
>
|
||||
<Plus width={22} height={22} fill='currentColor' />
|
||||
Add SSH Key
|
||||
</ActionButton>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -168,7 +166,7 @@ const AccountSSHContainer = () => {
|
||||
'linear(0,0.01,0.04 1.6%,0.161 3.3%,0.816 9.4%,1.046,1.189 14.4%,1.231,1.254 17%,1.259,1.257 18.6%,1.236,1.194 22.3%,1.057 27%,0.999 29.4%,0.955 32.1%,0.942,0.935 34.9%,0.933,0.939 38.4%,1 47.3%,1.011,1.017 52.6%,1.016 56.4%,1 65.2%,0.996 70.2%,1.001 87.2%,1)',
|
||||
}}
|
||||
>
|
||||
<div className='bg-gradient-to-b from-[#ffffff08] to-[#ffffff05] border-[1px] border-[#ffffff12] rounded-xl p-4 sm:p-6 shadow-sm'>
|
||||
<div className='bg-mocha-500 border-[1px] border-[#ffffff12] rounded-xl p-4 sm:p-6 shadow-sm'>
|
||||
<SpinnerOverlay visible={!data && isValidating} />
|
||||
<Dialog.Confirm
|
||||
title={'Delete SSH Key'}
|
||||
@@ -205,7 +203,7 @@ const AccountSSHContainer = () => {
|
||||
'linear(0,0.01,0.04 1.6%,0.161 3.3%,0.816 9.4%,1.046,1.189 14.4%,1.231,1.254 17%,1.259,1.257 18.6%,1.236,1.194 22.3%,1.057 27%,0.999 29.4%,0.955 32.1%,0.942,0.935 34.9%,0.933,0.939 38.4%,1 47.3%,1.011,1.017 52.6%,1.016 56.4%,1 65.2%,0.996 70.2%,1.001 87.2%,1)',
|
||||
}}
|
||||
>
|
||||
<div className='bg-[#ffffff05] border-[1px] border-[#ffffff08] rounded-lg p-4 hover:border-[#ffffff15] transition-all duration-150'>
|
||||
<div className=' rounded-lg transition-all duration-150'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex-1 min-w-0'>
|
||||
<div className='flex items-center gap-3 mb-2'>
|
||||
@@ -217,7 +215,7 @@ const AccountSSHContainer = () => {
|
||||
<span>Added: {format(key.createdAt, 'MMM d, yyyy HH:mm')}</span>
|
||||
<div className='flex items-center gap-2'>
|
||||
<span>Fingerprint:</span>
|
||||
<code className='font-mono px-2 py-1 bg-[#ffffff08] border border-[#ffffff08] rounded text-zinc-300'>
|
||||
<code className='font-mono px-2 py-1 bg-mocha-400 border border-mocha-200 rounded text-zinc-300'>
|
||||
{showKeys[key.fingerprint]
|
||||
? `SHA256:${key.fingerprint}`
|
||||
: 'SHA256:••••••••••••••••'}
|
||||
|
||||
@@ -510,8 +510,8 @@ const SoftwareContainer = () => {
|
||||
selectedDockerImage && eggPreview.docker_images
|
||||
? eggPreview.docker_images[selectedDockerImage]
|
||||
: eggPreview.default_docker_image && eggPreview.docker_images
|
||||
? eggPreview.docker_images[eggPreview.default_docker_image]
|
||||
: '';
|
||||
? eggPreview.docker_images[eggPreview.default_docker_image]
|
||||
: '';
|
||||
|
||||
// Filter out empty environment variables to prevent validation issues
|
||||
const filteredEnvironment: Record<string, string> = {};
|
||||
@@ -901,11 +901,10 @@ const SoftwareContainer = () => {
|
||||
handleVariableChange(variable.env_variable, e.target.value)
|
||||
}
|
||||
placeholder={variable.default_value || 'Enter value...'}
|
||||
className={`w-full px-3 py-2 bg-[#ffffff08] border rounded-lg text-sm text-neutral-200 placeholder:text-neutral-500 focus:outline-none transition-colors ${
|
||||
variableErrors[variable.env_variable]
|
||||
className={`w-full px-3 py-2 bg-[#ffffff08] border rounded-lg text-sm text-neutral-200 placeholder:text-neutral-500 focus:outline-none transition-colors ${variableErrors[variable.env_variable]
|
||||
? 'border-red-500 focus:border-red-500'
|
||||
: 'border-[#ffffff12] focus:border-brand'
|
||||
}`}
|
||||
}`}
|
||||
/>
|
||||
{variableErrors[variable.env_variable] && (
|
||||
<p className='text-xs text-red-400 mt-1'>
|
||||
@@ -946,11 +945,11 @@ const SoftwareContainer = () => {
|
||||
</label>
|
||||
<p className='text-xs text-neutral-400 leading-relaxed'>
|
||||
{backupLimit !== 0 &&
|
||||
(backupLimit === null || (backups?.backupCount || 0) < backupLimit)
|
||||
(backupLimit === null || (backups?.backupCount || 0) < backupLimit)
|
||||
? 'Automatically create a backup before applying changes'
|
||||
: backupLimit === 0
|
||||
? 'Backups are disabled for this server'
|
||||
: 'Backup limit reached'}
|
||||
? 'Backups are disabled for this server'
|
||||
: 'Backup limit reached'}
|
||||
</p>
|
||||
</div>
|
||||
<div className='flex-shrink-0'>
|
||||
@@ -1108,26 +1107,23 @@ const SoftwareContainer = () => {
|
||||
{eggPreview.warnings.map((warning, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`p-4 border rounded-lg ${
|
||||
warning.severity === 'error'
|
||||
className={`p-4 border rounded-lg ${warning.severity === 'error'
|
||||
? 'bg-red-500/10 border-red-500/20'
|
||||
: 'bg-amber-500/10 border-amber-500/20'
|
||||
}`}
|
||||
}`}
|
||||
>
|
||||
<div className='flex items-start gap-3'>
|
||||
<TriangleExclamation
|
||||
width={22}
|
||||
height={22}
|
||||
fill='currentColor'
|
||||
className={`w-5 h-5 flex-shrink-0 mt-0.5 ${
|
||||
warning.severity === 'error' ? 'text-red-400' : 'text-amber-400'
|
||||
}`}
|
||||
className={`w-5 h-5 flex-shrink-0 mt-0.5 ${warning.severity === 'error' ? 'text-red-400' : 'text-amber-400'
|
||||
}`}
|
||||
/>
|
||||
<div>
|
||||
<h4
|
||||
className={`font-semibold mb-2 ${
|
||||
warning.severity === 'error' ? 'text-red-400' : 'text-amber-400'
|
||||
}`}
|
||||
className={`font-semibold mb-2 ${warning.severity === 'error' ? 'text-red-400' : 'text-amber-400'
|
||||
}`}
|
||||
>
|
||||
{warning.type === 'subdomain_incompatible'
|
||||
? 'Subdomain Will Be Deleted'
|
||||
|
||||
@@ -49,7 +49,7 @@ const sheetVariants = cva(
|
||||
|
||||
interface SheetContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> {}
|
||||
VariantProps<typeof sheetVariants> { }
|
||||
|
||||
const SheetContent = React.forwardRef<React.ComponentRef<typeof SheetPrimitive.Content>, SheetContentProps>(
|
||||
({ side = 'right', className, children, ...props }, ref) => (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@extends('templates/wrapper', [
|
||||
'css' => ['body' => 'bg-black'],
|
||||
'css' => ['body' => 'bg-mocha-600'],
|
||||
])
|
||||
|
||||
@section('container')
|
||||
|
||||
Reference in New Issue
Block a user