Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
Skeleton,
Tooltip,
TooltipContent,
TooltipTrigger,
} from 'ui'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import {
Expand All @@ -26,8 +23,24 @@ import {
TableCell,
TableHead,
TableHeader,
TableHeadSort,
TableRow,
} from 'ui/src/components/shadcn/ui/table'
import { TimestampInfo } from 'ui-patterns/TimestampInfo'
import { parseAsStringLiteral, useQueryState } from 'nuqs'

const ACCESS_TOKEN_SORT_VALUES = [
'created_at:asc',
'created_at:desc',
'last_used_at:asc',
'last_used_at:desc',
'expires_at:asc',
'expires_at:desc',
] as const

type AccessTokenSort = (typeof ACCESS_TOKEN_SORT_VALUES)[number]
type AccessTokenSortColumn = AccessTokenSort extends `${infer Column}:${string}` ? Column : unknown
type AccessTokenSortOrder = AccessTokenSort extends `${string}:${infer Order}` ? Order : unknown

const RowLoading = () => (
<TableRow>
Expand All @@ -49,17 +62,31 @@ const RowLoading = () => (
</TableRow>
)

const tableHeaderClass = 'text-left font-mono uppercase text-xs text-foreground-lighter h-auto py-2'
const tableHeaderClass = 'text-left font-mono uppercase text-xs text-foreground-lighter py-2'

interface TableContainerProps {
children: React.ReactNode
sort: AccessTokenSort
onSortChange: (column: AccessTokenSortColumn) => void
}

const TableContainer = ({ children }: { children: React.ReactNode }) => (
const TableContainer = ({ children, sort, onSortChange }: TableContainerProps) => (
<Card className="w-full overflow-hidden">
<CardContent className="p-0">
<Table className="p-5 table-auto">
<TableHeader>
<TableRow className="bg-200">
<TableHead className={tableHeaderClass}>Token</TableHead>
<TableHead className={tableHeaderClass}>Last used</TableHead>
<TableHead className={tableHeaderClass}>Expires</TableHead>
<TableHead className={tableHeaderClass}>
<TableHeadSort column="last_used_at" currentSort={sort} onSortChange={onSortChange}>
Last used
</TableHeadSort>
</TableHead>
<TableHead className={tableHeaderClass}>
<TableHeadSort column="expires_at" currentSort={sort} onSortChange={onSortChange}>
Expires
</TableHeadSort>
</TableHead>
<TableHead className={cn(tableHeaderClass, '!text-right')} />
</TableRow>
</TableHeader>
Expand All @@ -77,6 +104,10 @@ export interface AccessTokenListProps {
export const AccessTokenList = ({ searchString = '', onDeleteSuccess }: AccessTokenListProps) => {
const [isOpen, setIsOpen] = useState(false)
const [token, setToken] = useState<AccessToken | undefined>(undefined)
const [sort, setSort] = useQueryState(
'sort',
parseAsStringLiteral<AccessTokenSort>(ACCESS_TOKEN_SORT_VALUES).withDefault('created_at:desc')
)

const { data: tokens, error, isPending: isLoading, isError } = useAccessTokensQuery()

Expand All @@ -91,25 +122,72 @@ export const AccessTokenList = ({ searchString = '', onDeleteSuccess }: AccessTo
},
})

const handleSortChange = (column: AccessTokenSortColumn) => {
const [currentCol, currentOrder] = sort.split(':') as [
AccessTokenSortColumn,
AccessTokenSortOrder,
]
if (currentCol === column) {
if (currentOrder === 'asc') {
setSort(`${column}:desc` as AccessTokenSort)
} else {
setSort('created_at:desc')
}
} else {
setSort(`${column}:asc` as AccessTokenSort)
}
}

const onDeleteToken = async (tokenId: number) => {
deleteToken({ id: tokenId })
}

const filteredTokens = useMemo(() => {
return !searchString
const filtered = !searchString
? tokens
: tokens?.filter((token) => {
return token.name.toLowerCase().includes(searchString.toLowerCase())
})
}, [tokens, searchString])

if (!filtered) return filtered

const [sortCol, sortOrder] = sort.split(':') as [AccessTokenSortColumn, AccessTokenSortOrder]
const orderMultiplier = sortOrder === 'asc' ? 1 : -1

return [...filtered].sort((a, b) => {
if (sortCol === 'created_at') {
return (
(new Date(a.created_at).getTime() - new Date(b.created_at).getTime()) * orderMultiplier
)
}
if (sortCol === 'last_used_at') {
if (!a.last_used_at && !b.last_used_at) return 0
if (!a.last_used_at) return 1
if (!b.last_used_at) return -1
return (
(new Date(a.last_used_at).getTime() - new Date(b.last_used_at).getTime()) *
orderMultiplier
)
}
if (sortCol === 'expires_at') {
if (!a.expires_at && !b.expires_at) return 0
if (!a.expires_at) return 1
if (!b.expires_at) return -1
return (
(new Date(a.expires_at).getTime() - new Date(b.expires_at).getTime()) * orderMultiplier
)
}
return 0
})
}, [tokens, searchString, sort])

const empty = filteredTokens?.length === 0 && !isLoading

if (isError) {
return (
<TableContainer>
<TableContainer sort={sort} onSortChange={handleSortChange}>
<TableRow>
<TableCell colSpan={5} className="p-0">
<TableCell colSpan={4} className="p-0">
<AlertError
error={error}
subject="Failed to retrieve access tokens"
Expand All @@ -123,7 +201,7 @@ export const AccessTokenList = ({ searchString = '', onDeleteSuccess }: AccessTo

if (isLoading) {
return (
<TableContainer>
<TableContainer sort={sort} onSortChange={handleSortChange}>
<RowLoading />
<RowLoading />
</TableContainer>
Expand All @@ -132,9 +210,9 @@ export const AccessTokenList = ({ searchString = '', onDeleteSuccess }: AccessTo

if (empty) {
return (
<TableContainer>
<TableContainer sort={sort} onSortChange={handleSortChange}>
<TableRow>
<TableCell colSpan={5} className="py-12">
<TableCell colSpan={4} className="py-12">
<p className="text-sm text-center text-foreground">No access tokens found</p>
<p className="text-sm text-center text-foreground-light">
You do not have any tokens created yet
Expand All @@ -147,54 +225,40 @@ export const AccessTokenList = ({ searchString = '', onDeleteSuccess }: AccessTo

return (
<>
<TableContainer>
<TableContainer sort={sort} onSortChange={handleSortChange}>
{filteredTokens?.map((x) => {
return (
<TableRow key={x.token_alias}>
<TableCell className="w-auto max-w-96">
<p className="truncate" title={x.name}>
{x.name}
</p>
<p className="font-mono text-foreground-lighter truncate text-xs mt-1">
<p
className="font-mono text-foreground-lighter truncate text-xs mt-1 max-w-32 sm:max-w-48 lg:max-w-full"
title={x.token_alias}
>
{x.token_alias}
</p>
</TableCell>
<TableCell className="min-w-28">
<p className="text-foreground-light">
{x.last_used_at ? (
<Tooltip>
<TooltipTrigger>{dayjs(x.last_used_at).format('DD MMM YYYY')}</TooltipTrigger>
<TooltipContent side="bottom">
Last used on {dayjs(x.last_used_at).format('DD MMM, YYYY HH:mm:ss')}
</TooltipContent>
</Tooltip>
) : (
'Never used'
)}
</p>
<TableCell className="text-foreground-light min-w-28">
{x.last_used_at ? (
<TimestampInfo
utcTimestamp={x.last_used_at}
label={dayjs(x.last_used_at).fromNow()}
/>
) : (
'Never used'
)}
</TableCell>
<TableCell className="min-w-28">
<TableCell className="min-w-28 text-foreground-light">
{x.expires_at ? (
dayjs(x.expires_at).isBefore(dayjs()) ? (
<Tooltip>
<TooltipTrigger>
<p className="text-foreground-light">Expired</p>
</TooltipTrigger>
<TooltipContent side="bottom">
Expired on {dayjs(x.expires_at).format('DD MMM, YYYY HH:mm:ss')}
</TooltipContent>
</Tooltip>
<TimestampInfo utcTimestamp={x.expires_at} label="Expired" />
) : (
<Tooltip>
<TooltipTrigger>
<p className="text-foreground-light">
{dayjs(x.expires_at).format('DD MMM YYYY')}
</p>
</TooltipTrigger>
<TooltipContent side="bottom">
Expires on {dayjs(x.expires_at).format('DD MMM, YYYY HH:mm:ss')}
</TooltipContent>
</Tooltip>
<TimestampInfo
utcTimestamp={x.expires_at}
label={dayjs(x.expires_at).format('DD MMM YYYY')}
/>
)
) : (
<p className="text-foreground-light">Never</p>
Expand All @@ -205,9 +269,9 @@ export const AccessTokenList = ({ searchString = '', onDeleteSuccess }: AccessTo
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="text"
type="default"
title="More options"
className="px-1.5"
className="px-1"
disabled={isLoading}
loading={isLoading}
icon={<MoreVertical />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export const UpdateRolesPanel = ({ visible, member, onClose }: UpdateRolesPanelP
const { data: allRoles, isSuccess: isSuccessRoles } = useOrganizationRolesV2Query({ slug })

const { data: projectsData } = useOrgProjectsInfiniteQuery({ slug })
const totalNumOrgProjects = projectsData?.pages[0].pagination.count ?? 0
const orgProjects =
useMemo(() => projectsData?.pages.flatMap((page) => page.projects), [projectsData?.pages]) || []

Expand Down Expand Up @@ -97,7 +98,6 @@ export const UpdateRolesPanel = ({ visible, member, onClose }: UpdateRolesPanelP
return !projectsRoleConfiguration.some((p) => p.ref === project.ref)
})
const numberOfProjectsWithAccess = orgProjects.length - noAccessProjects.length
const numberOfAccessHasChanges = originalConfiguration.length !== noAccessProjects.length
const hasNoChanges = isEqual(projectsRoleConfiguration, originalConfiguration)

const onSelectProject = (project: OrgProject) => {
Expand Down Expand Up @@ -206,7 +206,7 @@ export const UpdateRolesPanel = ({ visible, member, onClose }: UpdateRolesPanelP

{!isApplyingRoleToAllProjects &&
projectsRoleConfiguration.length > 0 &&
projectsRoleConfiguration.length !== orgProjects.length && (
projectsRoleConfiguration.length < totalNumOrgProjects && (
<Collapsible_Shadcn_ className="bg-alternative border rounded-lg py-4 group">
<CollapsibleTrigger_Shadcn_ className="w-full text-left px-4 flex items-center justify-between">
<span className="text-sm">
Expand Down
Loading
Loading