Skip to content

feat: animate layout transitions #474

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 77 additions & 76 deletions frontend/app/[team]/apps/[app]/_components/AppEnvironments.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
'use client'

import { GetAppEnvironments } from '@/graphql/queries/secrets/getAppEnvironments.gql'
import { SwapEnvOrder } from '@/graphql/mutations/environments/swapEnvironmentOrder.gql'
import { EnvironmentType, ApiOrganisationPlanChoices } from '@/apollo/graphql'
import { Button } from '@/components/common/Button'
import { Card } from '@/components/common/Card'
import { CreateEnvironmentDialog } from '@/components/environments/CreateEnvironmentDialog'
import { ManageEnvironmentDialog } from '@/components/environments/ManageEnvironmentDialog'
import { organisationContext } from '@/contexts/organisationContext'
import { userHasPermission } from '@/utils/access/permissions'
import { useMutation } from '@apollo/client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { useContext } from 'react'
import { BsListColumnsReverse } from 'react-icons/bs'
import { FaArrowRight, FaBan, FaExchangeAlt, FaFolder, FaKey } from 'react-icons/fa'
import { EmptyState } from '@/components/common/EmptyState'
import { useAppSecrets } from '../_hooks/useAppSecrets'
import { motion } from 'framer-motion'

export const AppEnvironments = ({ appId }: { appId: string }) => {
const { activeOrganisation: organisation } = useContext(organisationContext)
Expand All @@ -43,7 +41,7 @@ export const AppEnvironments = ({ appId }: { appId: string }) => {
true
)

const { appEnvironments, fetching } = useAppSecrets(
const { appEnvironments, swapEnvironments, fetching } = useAppSecrets(
appId,
userCanReadEnvironments,
10000 // Poll every 10 seconds
Expand All @@ -54,15 +52,6 @@ export const AppEnvironments = ({ appId }: { appId: string }) => {

const pathname = usePathname()

const [swapEnvs, { loading }] = useMutation(SwapEnvOrder)

const handleSwapEnvironments = async (env1: EnvironmentType, env2: EnvironmentType) => {
await swapEnvs({
variables: { environment1Id: env1.id, environment2Id: env2?.id },
refetchQueries: [{ query: GetAppEnvironments, variables: { appId } }],
})
}

return (
<section className="space-y-4 py-4">
<div className="space-y-2">
Expand All @@ -89,76 +78,88 @@ export const AppEnvironments = ({ appId }: { appId: string }) => {
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 py-4">
{appEnvironments?.map((env: EnvironmentType, index: number) => (
<Card key={env.id}>
<div className="group">
<div className="flex gap-4">
<div className="pt-1.5">
<BsListColumnsReverse className="text-black dark:text-white text-2xl" />
</div>
<div className="space-y-6 w-full min-w-0">
<div className="flex items-start justify-between gap-2">
<Link href={`${pathname}/environments/${env.id}`} className="group min-w-0">
<div className="font-semibold text-lg truncate">{env.name}</div>
<div className="text-neutral-500">
{/* Text-based secrets and folder count on wider screens */}
<div className="hidden lg:block">
{env.secretCount} secrets across {env.folderCount} folders
</div>
{/* Icon-based secrets and folder count on narrower screens */}
<div className="flex items-center gap-3 lg:hidden">
<div className="flex items-center gap-1.5" title={`${env.secretCount} secrets`}>
<FaKey className="text-sm" />
<span>{env.secretCount}</span>
<motion.div
key={env.id}
layout
transition={{ duration: 0.25, ease: 'easeOut', delay: 0.15 }}
>
<Card>
<div className="group">
<div className="flex gap-4">
<div className="pt-1.5">
<BsListColumnsReverse className="text-black dark:text-white text-2xl" />
</div>
<div className="space-y-6 w-full min-w-0">
<div className="flex items-start justify-between gap-2">
<Link href={`${pathname}/environments/${env.id}`} className="group min-w-0">
<div className="font-semibold text-lg truncate">{env.name}</div>
<div className="text-neutral-500">
{/* Text-based secrets and folder count on wider screens */}
<div className="hidden lg:block">
{env.secretCount} secrets across {env.folderCount} folders
</div>
<div className="flex items-center gap-1.5" title={`${env.folderCount} folders`}>
<FaFolder className="text-sm" />
<span>{env.folderCount}</span>
{/* Icon-based secrets and folder count on narrower screens */}
<div className="flex items-center gap-3 lg:hidden">
<div
className="flex items-center gap-1.5"
title={`${env.secretCount} secrets`}
>
<FaKey className="text-sm" />
<span>{env.secretCount}</span>
</div>
<div
className="flex items-center gap-1.5"
title={`${env.folderCount} folders`}
>
<FaFolder className="text-sm" />
<span>{env.folderCount}</span>
</div>
</div>
</div>
</div>
</Link>
<ManageEnvironmentDialog environment={env} />
</div>
</Link>
<ManageEnvironmentDialog environment={env} />
</div>

<div className="flex items-center">
<Link href={`${pathname}/environments/${env.id}`}>
<Button variant="primary">
Explore <FaArrowRight />
</Button>
</Link>
<div className="flex items-center">
<Link href={`${pathname}/environments/${env.id}`}>
<Button variant="primary">
Explore <FaArrowRight />
</Button>
</Link>
</div>
</div>
</div>
</div>
{allowReordering && (
<div className="flex justify-between items-center opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto">
<div>
{index !== 0 && (
<Button
variant="secondary"
disabled={loading}
title={`Swap with ${appEnvironments[index - 1].name}`}
onClick={() => handleSwapEnvironments(env, appEnvironments[index - 1])}
>
<FaExchangeAlt className="text-xs shrink-0" />
</Button>
)}
</div>
<div>
{index !== appEnvironments.length - 1 && (
<Button
variant="secondary"
disabled={loading}
title={`Swap with ${appEnvironments[index + 1].name}`}
onClick={() => handleSwapEnvironments(env, appEnvironments[index + 1])}
>
<FaExchangeAlt className="text-xs shrink-0" />
</Button>
)}
{allowReordering && (
<div className="flex justify-between items-center opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto">
<div>
{index !== 0 && (
<Button
variant="secondary"
disabled={fetching}
title={`Swap with ${appEnvironments[index - 1].name}`}
onClick={() => swapEnvironments(env.id, appEnvironments[index - 1].id)}
>
<FaExchangeAlt className="text-xs shrink-0" />
</Button>
)}
</div>
<div>
{index !== appEnvironments.length - 1 && (
<Button
variant="secondary"
disabled={fetching}
title={`Swap with ${appEnvironments[index + 1].name}`}
onClick={() => swapEnvironments(env.id, appEnvironments[index + 1].id)}
>
<FaExchangeAlt className="text-xs shrink-0" />
</Button>
)}
</div>
</div>
</div>
)}
</div>
</Card>
)}
</div>
</Card>
</motion.div>
))}

{userCanCreateEnvironments && (
Expand Down
79 changes: 43 additions & 36 deletions frontend/app/[team]/apps/[app]/_components/AppSecretRow.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { EnvironmentType, SecretType } from '@/apollo/graphql'
import { userHasPermission } from '@/utils/access/permissions'
import { Disclosure, Switch, Transition } from '@headlessui/react'
import { Disclosure, Switch } from '@headlessui/react'
import clsx from 'clsx'
import {
FaChevronRight,
Expand All @@ -25,6 +25,7 @@ import { usePathname } from 'next/navigation'
import { arraysEqual } from '@/utils/crypto'
import { toggleBooleanKeepingCase } from '@/utils/secrets'
import CopyButton from '@/components/common/CopyButton'
import { motion } from 'framer-motion'

const INPUT_BASE_STYLE =
'w-full font-mono custom bg-transparent group-hover:bg-zinc-400/20 dark:group-hover:bg-zinc-400/10 transition ease ph-no-capture'
Expand Down Expand Up @@ -447,10 +448,12 @@ export const AppSecretRow = ({
</div>
</td>
{envs.map((env) => (
<td
<motion.td
key={env.env.id}
className={'px-6 whitespace-nowrap group cursor-pointer'}
onClick={toggleAccordion}
layout
transition={{ duration: 0.25, ease: 'easeOut', delay: 0.15 }}
>
<div className="flex items-center justify-center" title={tooltipText(env)}>
{env.secret !== null ? (
Expand All @@ -468,18 +471,18 @@ export const AppSecretRow = ({
<FaTimesCircle className="text-red-500 shrink-0" />
)}
</div>
</td>
</motion.td>
))}
</tr>
<Transition
as="tr"
show={isExpanded}
enter="transition duration-150 ease-out"
enterFrom="transform opacity-0"
enterTo="transform opacity-100"
leave="transition duration-100 ease-out"
leaveFrom="transform opacity-100"
leaveTo="transform opacity-0"
<motion.tr
initial={{ opacity: 0, y: -5, maxHeight: 0 }}
animate={{
opacity: isExpanded ? 1 : 0,
y: isExpanded ? 0 : -5,
maxHeight: isExpanded ? '500px' : '0px',
}}
exit={{ opacity: 0, y: -5, maxHeight: '0px' }}
transition={{ duration: 0.25, ease: 'easeOut', delay: 0.15 }}
className={clsx(
'border-x',
isExpanded
Expand All @@ -490,33 +493,37 @@ export const AppSecretRow = ({
{isExpanded && (
<td
colSpan={clientAppSecret.envs.length + 1}
className={clsx('p-2 space-y-6 ', rowBgColorOpen())}
className={clsx('p-2 space-y-6', rowBgColorOpen())}
>
<Disclosure.Panel static={true}>
<div className={clsx('grid gap-2 divide-y divide-neutral-500/10')}>
{envs.map((envSecret) => (
<EnvSecret
key={envSecret.env.id}
keyIsStagedForDelete={stagedForDelete}
clientEnvSecret={envSecret}
serverEnvSecret={serverEnvSecret(envSecret.env?.id!)}
sameAsProd={secretIsSameAsProd(envSecret)}
appSecretId={clientAppSecret.id}
updateEnvValue={updateValue}
stagedForDelete={
envSecret.secret
? secretsStagedForDelete.includes(envSecret.secret?.id)
: false
}
addEnvValue={addEnvValue}
deleteEnvValue={deleteEnvValue}
/>
))}
</div>
</Disclosure.Panel>
<div
//initial={{ opacity: 0 }}
//animate={{ opacity: 1 }}
//exit={{ opacity: 0 }}
//transition={{ duration: 0.1, ease: 'easeOut' }}
className="grid gap-2 divide-y divide-neutral-500/10"
>
{envs.map((envSecret) => (
<EnvSecret
key={envSecret.env.id}
keyIsStagedForDelete={stagedForDelete}
clientEnvSecret={envSecret}
serverEnvSecret={serverEnvSecret(envSecret.env?.id!)}
sameAsProd={secretIsSameAsProd(envSecret)}
appSecretId={clientAppSecret.id}
updateEnvValue={updateValue}
stagedForDelete={
envSecret.secret
? secretsStagedForDelete.includes(envSecret.secret?.id)
: false
}
addEnvValue={addEnvValue}
deleteEnvValue={deleteEnvValue}
/>
))}
</div>
</td>
)}
</Transition>
</motion.tr>
</>
)}
</Disclosure>
Expand Down
7 changes: 5 additions & 2 deletions frontend/app/[team]/apps/[app]/_components/AppSecrets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import { formatTitle } from '@/utils/meta'
import MultiEnvImportDialog from '@/components/environments/secrets/import/MultiEnvImportDialog'
import { TbDownload } from 'react-icons/tb'
import { duplicateKeysExist } from '@/utils/secrets'
import { motion } from 'framer-motion'

export const AppSecrets = ({ team, app }: { team: string; app: string }) => {
const { activeOrganisation: organisation } = useContext(organisationContext)
Expand Down Expand Up @@ -837,9 +838,11 @@ export const AppSecrets = ({ team, app }: { team: string; app: string }) => {
key
</th>
{appEnvironments?.map((env: EnvironmentType) => (
<th
<motion.th
key={env.id}
className="group text-center text-sm font-semibold uppercase tracking-widest py-2"
layout
transition={{ duration: 0.25, ease: 'easeOut', delay: 0.15 }}
>
<Link href={`${pathname}/environments/${env.id}`}>
<Button variant="outline">
Expand All @@ -851,7 +854,7 @@ export const AppSecrets = ({ team, app }: { team: string; app: string }) => {
</div>
</Button>
</Link>
</th>
</motion.th>
))}
</tr>
</thead>
Expand Down
Loading