mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
Small refactor
This commit is contained in:
1 parent
3a6ed05990
commit
eff4373b6b
7 files changed
+229
-224
No files matched your search
@@ -1,8 +1,9 @@
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import dayjs from 'dayjs'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Alert, Button, IconArrowDown, IconArrowUp, IconRefreshCw } from 'ui'
|
||||
|
||||
import { ScaffoldContainerLegacy } from 'components/layouts/Scaffold'
|
||||
import { FilterPopover, LogDetailsPanel } from 'components/interfaces/AuditLogs'
|
||||
import Table from 'components/to-be-cleaned/Table'
|
||||
import AlertError from 'components/ui/AlertError'
|
||||
import { DatePicker } from 'components/ui/DatePicker'
|
||||
@@ -11,9 +12,6 @@ import { AuditLog } from 'data/organizations/organization-audit-logs-query'
|
||||
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
|
||||
import { useProfileAuditLogsQuery } from 'data/profile/profile-audit-logs-query'
|
||||
import { useProjectsQuery } from 'data/projects/projects-query'
|
||||
import { Alert, Button, IconArrowDown, IconArrowUp, IconRefreshCw } from 'ui'
|
||||
import FilterPopover from '../Organization/AuditLogs/FilterPopover'
|
||||
import LogDetailsPanel from '../Organization/AuditLogs/LogDetailsPanel'
|
||||
|
||||
const AuditLogs = () => {
|
||||
const currentTime = dayjs().utc().set('millisecond', 0)
|
||||
@@ -71,219 +69,215 @@ const AuditLogs = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScaffoldContainerLegacy>
|
||||
<div className="space-y-4 flex flex-col">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<p className="text-xs prose">Filter by</p>
|
||||
<FilterPopover
|
||||
name="Projects"
|
||||
options={projects ?? []}
|
||||
labelKey="name"
|
||||
valueKey="ref"
|
||||
activeOptions={filters.projects}
|
||||
onSaveFilters={(values) => setFilters({ ...filters, projects: values })}
|
||||
/>
|
||||
<DatePicker
|
||||
hideTime
|
||||
hideClear
|
||||
triggerButtonType="dashed"
|
||||
triggerButtonTitle=""
|
||||
from={dateRange.from}
|
||||
to={dateRange.to}
|
||||
minDate={dayjs().subtract(retentionPeriod, 'days').toDate()}
|
||||
maxDate={dayjs().toDate()}
|
||||
onChange={(value) => {
|
||||
if (value.from !== null && value.to !== null) {
|
||||
const current = dayjs().utc()
|
||||
const from = dayjs(value.from)
|
||||
.utc()
|
||||
.hour(current.hour())
|
||||
.minute(current.minute())
|
||||
.second(current.second())
|
||||
.toISOString()
|
||||
const to = dayjs(value.to)
|
||||
.utc()
|
||||
.hour(current.hour())
|
||||
.minute(current.minute())
|
||||
.second(current.second())
|
||||
.toISOString()
|
||||
setDateRange({ from, to })
|
||||
}
|
||||
}}
|
||||
renderFooter={() => {
|
||||
return (
|
||||
<Alert title="" variant="info" className="mx-3 pl-2 pr-2 pt-1 pb-2">
|
||||
You have a log retention period of{' '}
|
||||
<span className="text-brand">
|
||||
{retentionPeriod} day
|
||||
{retentionPeriod > 1 ? 's' : ''}
|
||||
</span>
|
||||
. You may only view logs from{' '}
|
||||
{dayjs().subtract(retentionPeriod, 'days').format('DD MMM YYYY')} as the
|
||||
earliest date.
|
||||
</Alert>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{isSuccess && (
|
||||
<>
|
||||
<div className="h-[20px] border-r border-scale-700 !ml-4 !mr-2" />
|
||||
<p className="prose text-xs">Viewing {sortedLogs.length} logs in total</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
type="default"
|
||||
disabled={isLoading || isRefetching}
|
||||
icon={<IconRefreshCw className={isRefetching ? 'animate-spin' : ''} />}
|
||||
onClick={() => refetch()}
|
||||
>
|
||||
{isRefetching ? 'Refreshing' : 'Refresh'}
|
||||
</Button>
|
||||
<div className="space-y-4 flex flex-col">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<p className="text-xs prose">Filter by</p>
|
||||
<FilterPopover
|
||||
name="Projects"
|
||||
options={projects ?? []}
|
||||
labelKey="name"
|
||||
valueKey="ref"
|
||||
activeOptions={filters.projects}
|
||||
onSaveFilters={(values) => setFilters({ ...filters, projects: values })}
|
||||
/>
|
||||
<DatePicker
|
||||
hideTime
|
||||
hideClear
|
||||
triggerButtonType="dashed"
|
||||
triggerButtonTitle=""
|
||||
from={dateRange.from}
|
||||
to={dateRange.to}
|
||||
minDate={dayjs().subtract(retentionPeriod, 'days').toDate()}
|
||||
maxDate={dayjs().toDate()}
|
||||
onChange={(value) => {
|
||||
if (value.from !== null && value.to !== null) {
|
||||
const current = dayjs().utc()
|
||||
const from = dayjs(value.from)
|
||||
.utc()
|
||||
.hour(current.hour())
|
||||
.minute(current.minute())
|
||||
.second(current.second())
|
||||
.toISOString()
|
||||
const to = dayjs(value.to)
|
||||
.utc()
|
||||
.hour(current.hour())
|
||||
.minute(current.minute())
|
||||
.second(current.second())
|
||||
.toISOString()
|
||||
setDateRange({ from, to })
|
||||
}
|
||||
}}
|
||||
renderFooter={() => {
|
||||
return (
|
||||
<Alert title="" variant="info" className="mx-3 pl-2 pr-2 pt-1 pb-2">
|
||||
You have a log retention period of{' '}
|
||||
<span className="text-brand">
|
||||
{retentionPeriod} day
|
||||
{retentionPeriod > 1 ? 's' : ''}
|
||||
</span>
|
||||
. You may only view logs from{' '}
|
||||
{dayjs().subtract(retentionPeriod, 'days').format('DD MMM YYYY')} as the
|
||||
earliest date.
|
||||
</Alert>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{isSuccess && (
|
||||
<>
|
||||
<div className="h-[20px] border-r border-scale-700 !ml-4 !mr-2" />
|
||||
<p className="prose text-xs">Viewing {sortedLogs.length} logs in total</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isLoading && (
|
||||
<div className="space-y-2">
|
||||
<ShimmeringLoader />
|
||||
<ShimmeringLoader className="w-3/4" />
|
||||
<ShimmeringLoader className="w-1/2" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isError && <AlertError error={error} subject="Failed to retrieve audit logs" />}
|
||||
|
||||
{isSuccess && (
|
||||
<>
|
||||
{logs.length === 0 ? (
|
||||
<div className="bg-scale-100 dark:bg-scale-300 border rounded p-4 flex items-center justify-between">
|
||||
<p className="prose text-sm">You do not have any audit logs available yet</p>
|
||||
</div>
|
||||
) : logs.length > 0 && sortedLogs.length === 0 ? (
|
||||
<div className="bg-scale-100 dark:bg-scale-300 border rounded p-4 flex items-center justify-between">
|
||||
<p className="prose text-sm">No audit logs found based on the filters applied</p>
|
||||
</div>
|
||||
) : (
|
||||
<Table
|
||||
head={[
|
||||
<Table.th key="action" className="py-2">
|
||||
Action
|
||||
</Table.th>,
|
||||
<Table.th key="target" className="py-2">
|
||||
Target
|
||||
</Table.th>,
|
||||
<Table.th key="date" className="py-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<p>Date</p>
|
||||
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
type="text"
|
||||
className="px-1"
|
||||
icon={
|
||||
dateSortDesc ? (
|
||||
<IconArrowDown strokeWidth={1.5} size={14} />
|
||||
) : (
|
||||
<IconArrowUp strokeWidth={1.5} size={14} />
|
||||
)
|
||||
}
|
||||
onClick={() => setDateSortDesc(!dateSortDesc)}
|
||||
/>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content side="right">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-scale-100 py-1 px-2 leading-none shadow',
|
||||
'border border-scale-200',
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs text-scale-1200">
|
||||
{dateSortDesc ? 'Sort latest first' : 'Sort earliest first'}
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
</Table.th>,
|
||||
<Table.th key="actions" className="py-2"></Table.th>,
|
||||
]}
|
||||
body={
|
||||
sortedLogs?.map((log) => {
|
||||
const project = projects?.find(
|
||||
(project) => project.ref === log.target.metadata.project_ref
|
||||
)
|
||||
const organization = organizations?.find(
|
||||
(org) => org.slug === log.target.metadata.org_slug
|
||||
)
|
||||
|
||||
const hasStatusCode = log.action.metadata[0]?.status !== undefined
|
||||
|
||||
return (
|
||||
<Table.tr
|
||||
key={log.occurred_at}
|
||||
onClick={() => setSelectedLog(log)}
|
||||
className="cursor-pointer hover:!bg-scale-100 transition duration-100"
|
||||
>
|
||||
<Table.td>
|
||||
<div className="flex items-center space-x-2">
|
||||
{hasStatusCode && (
|
||||
<p className="bg-scale-400 rounded px-1 flex items-center justify-center text-xs font-mono border">
|
||||
{log.action.metadata[0].status}
|
||||
</p>
|
||||
)}
|
||||
<p className="max-w-[170px] truncate" title={log.action.name}>
|
||||
{log.action.name}
|
||||
</p>
|
||||
</div>
|
||||
</Table.td>
|
||||
<Table.td>
|
||||
<p
|
||||
className="text-scale-1100 max-w-[230px] truncate"
|
||||
title={project?.name ?? organization?.name ?? 'Unknown'}
|
||||
>
|
||||
{project?.name
|
||||
? 'Project: '
|
||||
: organization?.name
|
||||
? 'Organization: '
|
||||
: null}
|
||||
{project?.name ?? organization?.name ?? 'Unknown'}
|
||||
</p>
|
||||
<p
|
||||
className="text-scale-1000 text-xs mt-0.5 truncate"
|
||||
title={
|
||||
log.target.metadata.project_ref ?? log.target.metadata.org_slug
|
||||
}
|
||||
>
|
||||
{log.target.metadata.project_ref
|
||||
? 'Ref: '
|
||||
: log.target.metadata.org_slug
|
||||
? 'Slug: '
|
||||
: null}
|
||||
{log.target.metadata.project_ref ?? log.target.metadata.org_slug}
|
||||
</p>
|
||||
</Table.td>
|
||||
<Table.td>
|
||||
{dayjs(log.occurred_at).format('DD MMM YYYY, HH:mm:ss')}
|
||||
</Table.td>
|
||||
<Table.td align="right">
|
||||
<Button type="default">View details</Button>
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
)
|
||||
}) ?? []
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
type="default"
|
||||
disabled={isLoading || isRefetching}
|
||||
icon={<IconRefreshCw className={isRefetching ? 'animate-spin' : ''} />}
|
||||
onClick={() => refetch()}
|
||||
>
|
||||
{isRefetching ? 'Refreshing' : 'Refresh'}
|
||||
</Button>
|
||||
</div>
|
||||
</ScaffoldContainerLegacy>
|
||||
|
||||
{isLoading && (
|
||||
<div className="space-y-2">
|
||||
<ShimmeringLoader />
|
||||
<ShimmeringLoader className="w-3/4" />
|
||||
<ShimmeringLoader className="w-1/2" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isError && <AlertError error={error} subject="Failed to retrieve audit logs" />}
|
||||
|
||||
{isSuccess && (
|
||||
<>
|
||||
{logs.length === 0 ? (
|
||||
<div className="bg-scale-100 dark:bg-scale-300 border rounded p-4 flex items-center justify-between">
|
||||
<p className="prose text-sm">You do not have any audit logs available yet</p>
|
||||
</div>
|
||||
) : logs.length > 0 && sortedLogs.length === 0 ? (
|
||||
<div className="bg-scale-100 dark:bg-scale-300 border rounded p-4 flex items-center justify-between">
|
||||
<p className="prose text-sm">No audit logs found based on the filters applied</p>
|
||||
</div>
|
||||
) : (
|
||||
<Table
|
||||
head={[
|
||||
<Table.th key="action" className="py-2">
|
||||
Action
|
||||
</Table.th>,
|
||||
<Table.th key="target" className="py-2">
|
||||
Target
|
||||
</Table.th>,
|
||||
<Table.th key="date" className="py-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<p>Date</p>
|
||||
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
type="text"
|
||||
className="px-1"
|
||||
icon={
|
||||
dateSortDesc ? (
|
||||
<IconArrowDown strokeWidth={1.5} size={14} />
|
||||
) : (
|
||||
<IconArrowUp strokeWidth={1.5} size={14} />
|
||||
)
|
||||
}
|
||||
onClick={() => setDateSortDesc(!dateSortDesc)}
|
||||
/>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content side="right">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-scale-100 py-1 px-2 leading-none shadow',
|
||||
'border border-scale-200',
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs text-scale-1200">
|
||||
{dateSortDesc ? 'Sort latest first' : 'Sort earliest first'}
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
</Table.th>,
|
||||
<Table.th key="actions" className="py-2"></Table.th>,
|
||||
]}
|
||||
body={
|
||||
sortedLogs?.map((log) => {
|
||||
const project = projects?.find(
|
||||
(project) => project.ref === log.target.metadata.project_ref
|
||||
)
|
||||
const organization = organizations?.find(
|
||||
(org) => org.slug === log.target.metadata.org_slug
|
||||
)
|
||||
|
||||
const hasStatusCode = log.action.metadata[0]?.status !== undefined
|
||||
|
||||
return (
|
||||
<Table.tr
|
||||
key={log.occurred_at}
|
||||
onClick={() => setSelectedLog(log)}
|
||||
className="cursor-pointer hover:!bg-scale-100 transition duration-100"
|
||||
>
|
||||
<Table.td className="max-w-[250px]">
|
||||
<div className="flex items-center space-x-2">
|
||||
{hasStatusCode && (
|
||||
<p className="bg-scale-400 rounded px-1 flex items-center justify-center text-xs font-mono border">
|
||||
{log.action.metadata[0].status}
|
||||
</p>
|
||||
)}
|
||||
<p className="truncate" title={log.action.name}>
|
||||
{log.action.name}
|
||||
</p>
|
||||
</div>
|
||||
</Table.td>
|
||||
<Table.td>
|
||||
<p
|
||||
className="text-scale-1100 max-w-[230px] truncate"
|
||||
title={project?.name ?? organization?.name ?? 'Unknown'}
|
||||
>
|
||||
{project?.name
|
||||
? 'Project: '
|
||||
: organization?.name
|
||||
? 'Organization: '
|
||||
: null}
|
||||
{project?.name ?? organization?.name ?? 'Unknown'}
|
||||
</p>
|
||||
<p
|
||||
className="text-scale-1000 text-xs mt-0.5 truncate"
|
||||
title={log.target.metadata.project_ref ?? log.target.metadata.org_slug}
|
||||
>
|
||||
{log.target.metadata.project_ref
|
||||
? 'Ref: '
|
||||
: log.target.metadata.org_slug
|
||||
? 'Slug: '
|
||||
: null}
|
||||
{log.target.metadata.project_ref ?? log.target.metadata.org_slug}
|
||||
</p>
|
||||
</Table.td>
|
||||
<Table.td>
|
||||
{dayjs(log.occurred_at).format('DD MMM YYYY, HH:mm:ss')}
|
||||
</Table.td>
|
||||
<Table.td align="right">
|
||||
<Button type="default">View details</Button>
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
)
|
||||
}) ?? []
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<LogDetailsPanel selectedLog={selectedLog} onClose={() => setSelectedLog(undefined)} />
|
||||
</>
|
||||
|
||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,4 @@
|
||||
import FilterPopover from './FilterPopover'
|
||||
import LogDetailsPanel from './LogDetailsPanel'
|
||||
|
||||
export { FilterPopover, LogDetailsPanel }
|
||||
@@ -2,8 +2,10 @@ import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import dayjs from 'dayjs'
|
||||
import Image from 'next/image'
|
||||
import { useState } from 'react'
|
||||
import { Alert, Button, IconArrowDown, IconArrowUp, IconRefreshCw, IconUser } from 'ui'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { FilterPopover, LogDetailsPanel } from 'components/interfaces/AuditLogs'
|
||||
import { ScaffoldContainerLegacy } from 'components/layouts/Scaffold'
|
||||
import Table from 'components/to-be-cleaned/Table'
|
||||
import AlertError from 'components/ui/AlertError'
|
||||
@@ -17,9 +19,6 @@ import { useOrganizationDetailQuery } from 'data/organizations/organization-deta
|
||||
import { useOrganizationRolesQuery } from 'data/organizations/organization-roles-query'
|
||||
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
|
||||
import { useProjectsQuery } from 'data/projects/projects-query'
|
||||
import { Alert, Button, IconArrowDown, IconArrowUp, IconRefreshCw, IconUser } from 'ui'
|
||||
import FilterPopover from './FilterPopover'
|
||||
import LogDetailsPanel from './LogDetailsPanel'
|
||||
|
||||
// [Joshen considerations]
|
||||
// - Maybe fix the height of the table to the remaining height of the viewport, so that the search input is always visible
|
||||
@@ -283,14 +282,14 @@ const AuditLogs = () => {
|
||||
</div>
|
||||
</div>
|
||||
</Table.td>
|
||||
<Table.td>
|
||||
<Table.td className="max-w-[250px]">
|
||||
<div className="flex items-center space-x-2">
|
||||
{hasStatusCode && (
|
||||
<p className="bg-scale-400 rounded px-1 flex items-center justify-center text-xs font-mono border">
|
||||
{log.action.metadata[0].status}
|
||||
</p>
|
||||
)}
|
||||
<p className="max-w-[170px] truncate" title={log.action.name}>
|
||||
<p className="truncate" title={log.action.name}>
|
||||
{log.action.name}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -274,7 +274,7 @@ function _DatePicker({
|
||||
{children !== undefined ? (
|
||||
children
|
||||
) : (
|
||||
<span>
|
||||
<>
|
||||
{/* Custom */}
|
||||
{selectsRange &&
|
||||
appliedStartDate &&
|
||||
@@ -289,7 +289,7 @@ function _DatePicker({
|
||||
) : (
|
||||
'Custom'
|
||||
)}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</Popover>
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
import { AuditLogs } from 'components/interfaces/Account'
|
||||
import { AccountLayout } from 'components/layouts'
|
||||
import { ScaffoldContainerLegacy } from 'components/layouts/Scaffold'
|
||||
import { FormHeader } from 'components/ui/Forms'
|
||||
import { NextPageWithLayout } from 'types'
|
||||
|
||||
const User: NextPageWithLayout = () => {
|
||||
return (
|
||||
<div className="my-2">
|
||||
<ScaffoldContainerLegacy className="gap-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<FormHeader
|
||||
title="Account audit logs"
|
||||
description="View the audit log trail of actions made from your account"
|
||||
/>
|
||||
</div>
|
||||
<AuditLogs />
|
||||
</div>
|
||||
</ScaffoldContainerLegacy>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user