Files
Ivan VasilovandJordi Enric 56de26fe22 chore: Migrate the monorepo to use Tailwind v4 (#45318)
This PR migrates the whole monorepo to use Tailwind v4:
- Removed `@tailwindcss/container-queries` plugin since it's included by
default in v4,
- Bump all instances of Tailwind to v4. Made minimal changes to the
shared config to remove non-supported features (`alpha` mentions),
- Migrate all apps to be compatible with v4 configs,
- Fix the `typography.css` import in 3 apps,
- Add missing rules which were included by default in v3,
- Run `pnpm dlx @tailwindcss/upgrade` on all apps, which renames a lot
of classes
- Rename all misnamed classes according to
https://tailwindcss.com/docs/upgrade-guide#renamed-utilities in all
apps.

---------

Co-authored-by: Jordi Enric <jordi.err@gmail.com>
2026-04-30 10:53:24 +00:00

425 lines
17 KiB
TypeScript

import type { components } from 'api-types'
import { Copy, Download, Key, Plus, RotateCcw, X } from 'lucide-react'
import { useEffect, useRef, useState } from 'react'
import { toast } from 'sonner'
import {
Button,
Checkbox,
Command_Shadcn_,
CommandEmpty_Shadcn_,
CommandGroup_Shadcn_,
CommandInput_Shadcn_,
CommandItem_Shadcn_,
CommandList_Shadcn_,
Input_Shadcn_,
Popover_Shadcn_,
PopoverContent_Shadcn_,
PopoverTrigger_Shadcn_,
ScrollArea,
Sheet,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
WarningIcon,
} from 'ui'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { FormLayout } from 'ui-patterns/form/Layout/FormLayout'
import { usePrivateApps } from '../../PrivateAppsContext'
import { PERMISSIONS } from '../Apps.constants'
import { CreateAppSheetStep } from './CreateAppSheetStep'
import { usePlatformAppCreateMutation } from '@/data/platform-apps/platform-app-create-mutation'
import { usePlatformAppDeleteMutation } from '@/data/platform-apps/platform-app-delete-mutation'
import { usePlatformAppInstallationCreateMutation } from '@/data/platform-apps/platform-app-installation-create-mutation'
import { usePlatformAppSigningKeyCreateMutation } from '@/data/platform-apps/platform-app-signing-key-create-mutation'
import { useCopyToClipboard } from '@/hooks/ui/useCopyToClipboard'
type CreatePlatformAppResponse = components['schemas']['CreatePlatformAppResponse']
type CreatePlatformAppSigningKeyResponse =
components['schemas']['CreatePlatformAppSigningKeyResponse']
interface CreateAppSheetProps {
visible: boolean
onClose: () => void
onCreated: (app: CreatePlatformAppResponse) => void
}
export function CreateAppSheet({ visible, onClose, onCreated }: CreateAppSheetProps) {
const { slug, installations, addInstallation } = usePrivateApps()
const { copy } = useCopyToClipboard()
const [name, setName] = useState('')
const [selectedPermissions, setSelectedPermissions] = useState<string[]>([])
const [permissionSearchOpen, setPermissionSearchOpen] = useState(false)
const [createdApp, setCreatedApp] = useState<CreatePlatformAppResponse | null>(null)
const [generatedKey, setGeneratedKey] = useState<CreatePlatformAppSigningKeyResponse | null>(null)
const [keyCopied, setKeyCopied] = useState(false)
const [showCloseConfirm, setShowCloseConfirm] = useState(false)
const step3Ref = useRef<HTMLDivElement>(null)
const { mutate: deleteApp, isPending: isDeleting } = usePlatformAppDeleteMutation({
onSuccess: () => handleClose(),
})
const { mutate: installApp } = usePlatformAppInstallationCreateMutation({
onSuccess: (data) => {
if (data) addInstallation(data, 'all')
},
})
const { mutate: createSigningKey, isPending: isCreatingKey } =
usePlatformAppSigningKeyCreateMutation({
onSuccess: (keyData, vars) => {
if (keyData) setGeneratedKey(keyData)
if (installations.length === 0 && slug) {
installApp({ slug, app_id: vars.appId })
}
},
})
const { mutate: createApp, isPending: isCreatingApp } = usePlatformAppCreateMutation({
onSuccess: (data) => {
toast.success(`App "${data.name}" created`)
setCreatedApp(data)
createSigningKey({ slug: slug!, appId: data.id })
},
})
function reset() {
setName('')
setSelectedPermissions([])
setPermissionSearchOpen(false)
setCreatedApp(null)
setGeneratedKey(null)
setKeyCopied(false)
}
function handleClose() {
reset()
onClose()
}
function handleRequestClose() {
setShowCloseConfirm(true)
}
function handleCreate() {
if (!slug) return
createApp({
slug,
name: name.trim(),
permissions:
selectedPermissions as components['schemas']['CreatePlatformAppBody']['permissions'],
})
}
function toggle(id: string) {
setSelectedPermissions((prev) =>
prev.includes(id) ? prev.filter((p) => p !== id) : [...prev, id]
)
}
const canCreate = name.trim().length > 0 && selectedPermissions.length > 0
const isLoading = isCreatingApp || isCreatingKey || isDeleting
const keyRevealed = generatedKey !== null
useEffect(() => {
if (generatedKey) {
step3Ref.current?.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
}, [generatedKey])
return (
<>
<Sheet
open={visible}
onOpenChange={(open) => {
if (!open) handleRequestClose()
}}
>
<SheetContent
showClose={false}
size="default"
className="min-w-[600px]! flex flex-col h-full gap-0"
>
<SheetHeader>
<SheetTitle>Create private app</SheetTitle>
<SheetDescription className="sr-only">
Create a private app to generate scoped access tokens for your organization.
</SheetDescription>
</SheetHeader>
<ScrollArea className="flex-1 max-h-[calc(100vh-116px)]">
<div className="px-5 sm:px-6 py-6">
<CreateAppSheetStep
number={1}
title="App details"
description="The first app you create will be automatically installed."
disabled={keyRevealed}
>
<FormLayout label="Name" id="app-name">
<Input_Shadcn_
id="app-name"
placeholder="My integration"
value={name}
onChange={(e) => setName(e.target.value)}
disabled={isLoading}
/>
</FormLayout>
</CreateAppSheetStep>
<CreateAppSheetStep
number={2}
title="Permissions"
description="Select the permissions this app requires."
disabled={keyRevealed}
>
<div className="space-y-4">
<div className="flex justify-between items-center">
<span className="text-sm">Configure permissions</span>
<div className="flex items-center gap-2">
{selectedPermissions.length > 0 && (
<Button
type="default"
size="tiny"
className="p-1"
icon={<RotateCcw size={16} />}
onClick={() => setSelectedPermissions([])}
disabled={isLoading}
/>
)}
<Popover_Shadcn_
open={permissionSearchOpen}
onOpenChange={setPermissionSearchOpen}
modal
>
<PopoverTrigger_Shadcn_ asChild>
<Button
type="default"
size="tiny"
icon={<Plus size={14} />}
disabled={isLoading}
>
Add permission
</Button>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_ className="w-[400px] p-0" align="end">
<Command_Shadcn_>
<CommandInput_Shadcn_ placeholder="Search permissions..." />
<CommandList_Shadcn_>
<CommandEmpty_Shadcn_>No permissions found.</CommandEmpty_Shadcn_>
<CommandGroup_Shadcn_ className="[&>div]:text-left">
<div className="max-h-[210px] overflow-y-auto">
{PERMISSIONS.map((perm) => (
<CommandItem_Shadcn_
key={perm.id}
value={`${perm.id} ${perm.label}`}
onSelect={() => toggle(perm.id)}
className="text-foreground"
>
<div className="flex items-center gap-3 w-full">
<Checkbox
checked={selectedPermissions.includes(perm.id)}
onCheckedChange={() => toggle(perm.id)}
onClick={(e) => e.stopPropagation()}
/>
<Key size={12} className="text-foreground-lighter" />
<span className="font-medium text-foreground">
{perm.label}
</span>
</div>
</CommandItem_Shadcn_>
))}
</div>
</CommandGroup_Shadcn_>
</CommandList_Shadcn_>
</Command_Shadcn_>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
</div>
</div>
{selectedPermissions.length === 0 ? (
<div className="text-center py-8 border border-dashed border-border rounded-lg">
<p className="text-sm text-foreground-light">
No permissions configured yet.
</p>
</div>
) : (
<div className="border border-border rounded-lg">
{selectedPermissions.map((id, index) => {
const perm = PERMISSIONS.find((p) => p.id === id)
return (
<div key={id}>
<div className="flex items-center gap-3 p-3">
<div className="flex-1">
<p className="text-sm font-medium">{perm?.label}</p>
{perm?.description && (
<p className="text-xs text-foreground-lighter">
{perm.description}
</p>
)}
</div>
<Button
type="text"
size="tiny"
className="p-1"
icon={<X size={16} />}
disabled={isLoading}
onClick={() =>
setSelectedPermissions((prev) => prev.filter((p) => p !== id))
}
/>
</div>
{index < selectedPermissions.length - 1 && (
<div className="border-t border-border" />
)}
</div>
)
})}
</div>
)}
<div className="w-full flex gap-x-2 items-center">
<WarningIcon />
<span className="text-xs text-foreground-lighter">
Once you've set these permissions, you cannot edit them.
</span>
</div>
</div>
</CreateAppSheetStep>
<div ref={step3Ref}>
<CreateAppSheetStep
number={3}
title="Signing key"
description={
isCreatingKey
? 'Generating your signing key...'
: keyRevealed
? 'This is the only time you can view this key. Copy or download it and store it securely.'
: "A signing key will be generated automatically when you create the app. You'll only be able to view it once."
}
isLast
disabled={createdApp === null && !isLoading}
>
{isCreatingKey && (
<p className="text-sm text-foreground-light animate-pulse">
Generating signing key...
</p>
)}
{keyRevealed && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium">Private key</h3>
<div className="flex items-center gap-2">
<Button
type="default"
size="tiny"
icon={<Copy size={12} />}
onClick={() => copy(generatedKey.private_key, { withToast: true })}
>
Copy
</Button>
<Button
type="default"
size="tiny"
icon={<Download size={12} />}
onClick={() => {
const blob = new Blob([generatedKey.private_key], {
type: 'text/plain',
})
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `${createdApp?.name.toLowerCase().replace(/\s+/g, '-')}-private-key.pem`
a.click()
URL.revokeObjectURL(url)
}}
>
Download
</Button>
</div>
</div>
<textarea
readOnly
value={generatedKey.private_key}
rows={8}
className="w-full rounded-md border border-control bg-surface-200 px-3 py-2 text-xs font-mono resize-none focus:outline-hidden"
/>
<label className="flex items-center gap-3 cursor-pointer bg-warning-200 border border-warning-400 rounded-md px-3 py-2">
<Checkbox
id="key-copied"
checked={keyCopied}
onCheckedChange={(v) => setKeyCopied(Boolean(v))}
/>
<span className="text-sm text-warning cursor-pointer select-none">
I have copied the key and stored it securely
</span>
</label>
</div>
)}
</CreateAppSheetStep>
</div>
</div>
</ScrollArea>
<SheetFooter className="justify-end! w-full mt-auto py-4 border-t">
<div className="flex gap-2">
<Button type="default" onClick={handleRequestClose} disabled={isLoading}>
Cancel
</Button>
{keyRevealed ? (
<Button
type="primary"
disabled={!keyCopied}
onClick={() => {
onCreated(createdApp!)
handleClose()
}}
>
Done
</Button>
) : (
<Button
type="primary"
disabled={!canCreate}
loading={isLoading}
onClick={handleCreate}
>
Create app
</Button>
)}
</div>
</SheetFooter>
</SheetContent>
</Sheet>
<ConfirmationModal
variant="destructive"
visible={showCloseConfirm}
title="Close without saving?"
confirmLabel="Close"
confirmLabelLoading="Closing..."
onCancel={() => setShowCloseConfirm(false)}
onConfirm={() => {
setShowCloseConfirm(false)
if (createdApp && slug) {
deleteApp({ slug, appId: createdApp.id })
} else {
handleClose()
}
}}
>
<p className="text-sm text-foreground-light py-2">
{createdApp
? 'The app will be deleted and your signing key will be permanently lost.'
: 'Any progress you have made will be lost.'}
</p>
</ConfirmationModal>
</>
)
}