ui: Account Api/SSH Keys pages with new colros and changes

This commit is contained in:
Naterfute
2026-02-07 09:26:17 -08:00
parent f0656a77f5
commit bc3391a911
8 changed files with 102 additions and 78 deletions

View File

@@ -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;

View 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;

View File

@@ -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
: '••••••••••••••••'}

View File

@@ -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'}

View File

@@ -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:••••••••••••••••'}

View File

@@ -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'

View File

@@ -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) => (

View File

@@ -1,5 +1,5 @@
@extends('templates/wrapper', [
'css' => ['body' => 'bg-black'],
'css' => ['body' => 'bg-mocha-600'],
])
@section('container')