From 52c71256c585d0cec4d594fce59fcb6616c9fe06 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Fri, 23 Jun 2023 16:45:27 +0800 Subject: [PATCH 01/22] Implement audit logs --- .../Organization/AuditLogs/AuditLogs.tsx | 148 ++++++++++++++++++ .../AuditLogs/LogDetailsPanel.tsx | 96 ++++++++++++ .../interfaces/Organization/index.ts | 3 +- studio/components/ui/AlertError.tsx | 38 +++++ studio/data/organizations/keys.ts | 1 + .../organization-audit-logs-query.ts | 85 ++++++++++ studio/pages/org/[slug]/audit.tsx | 57 +++++++ 7 files changed, 427 insertions(+), 1 deletion(-) create mode 100644 studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx create mode 100644 studio/components/interfaces/Organization/AuditLogs/LogDetailsPanel.tsx create mode 100644 studio/components/ui/AlertError.tsx create mode 100644 studio/data/organizations/organization-audit-logs-query.ts create mode 100644 studio/pages/org/[slug]/audit.tsx diff --git a/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx b/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx new file mode 100644 index 00000000000..ecb23296b69 --- /dev/null +++ b/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx @@ -0,0 +1,148 @@ +import dayjs from 'dayjs' +import Image from 'next/image' +import { useState } from 'react' + +import { useParams } from 'common' +import Table from 'components/to-be-cleaned/Table' +import ShimmeringLoader from 'components/ui/ShimmeringLoader' +import { + OrganizationAuditLog, + useOrganizationAuditLogsQuery, +} from 'data/organizations/organization-audit-logs-query' +import { useOrganizationDetailQuery } from 'data/organizations/organization-detail-query' +import { useOrganizationRolesQuery } from 'data/organizations/organization-roles-query' +import { useOrganizationsQuery } from 'data/organizations/organizations-query' +import { useProjectsQuery } from 'data/projects/projects-query' +import { IconSearch, IconUser, Input } from 'ui' +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 + +const AuditLogs = () => { + const { slug } = useParams() + const currentTime = dayjs() + const [selectedLog, setSelectedLog] = useState() + + const { data: projects } = useProjectsQuery() + const { data: organizations } = useOrganizationsQuery() + const { data: detailData } = useOrganizationDetailQuery({ slug }) + const { data: rolesData } = useOrganizationRolesQuery({ slug }) + const { + data: logs, + isLoading, + isSuccess, + isError, + } = useOrganizationAuditLogsQuery({ + slug, + iso_timestamp_start: currentTime.subtract(1, 'day').toISOString(), + iso_timestamp_end: currentTime.toISOString(), + }) + + const members = detailData?.members ?? [] + const roles = rolesData?.roles ?? [] + const sortedLogs = logs?.sort( + (a, b) => Number(new Date(b.timestamp)) - Number(new Date(a.timestamp)) + ) + + return ( + <> +
+ {isLoading && ( +
+ + + +
+ )} + + {isSuccess && ( + <> +
+ } + placeholder="Search audit logs" + /> +
+ User, + Action, + Target, + Date, + ]} + body={ + sortedLogs?.map((log) => { + const user = members.find((member) => member.gotrue_id === log.actor.id) + const role = roles.find((role) => user?.role_ids?.[0] === role.id) + const project = projects?.find( + (project) => project.ref === log.permission_group.project_ref + ) + const organization = organizations?.find( + (org) => org.slug === log.permission_group.org_slug + ) + + const userIcon = + user?.invited_id || user?.username === user?.primary_email ? ( +
+ +
+ ) : ( + + ) + + return ( + setSelectedLog(log)} + className="cursor-pointer hover:!bg-scale-100 transition duration-100" + > + +
+ {userIcon} +
+

{user?.username ?? log.actor.id}

+

{role?.name}

+
+
+
+ +
+

+ {log.action.metadata[0].status} +

+

{log.action.name}

+
+
+ +

+ {project?.name ?? organization?.name ?? 'Entity no longer exists'} +

+

+ {log.permission_group.org_slug ?? log.permission_group.project_ref} +

+
+ {dayjs(log.timestamp).format('DD MMM YYYY, HH:mm:ss')} +
+ ) + }) ?? [] + } + /> + + )} + + + setSelectedLog(undefined)} /> + + ) +} + +export default AuditLogs diff --git a/studio/components/interfaces/Organization/AuditLogs/LogDetailsPanel.tsx b/studio/components/interfaces/Organization/AuditLogs/LogDetailsPanel.tsx new file mode 100644 index 00000000000..34bbf7f9c35 --- /dev/null +++ b/studio/components/interfaces/Organization/AuditLogs/LogDetailsPanel.tsx @@ -0,0 +1,96 @@ +import { useParams } from 'common' +import { FormSection, FormSectionContent, FormSectionLabel } from 'components/ui/Forms' +import { OrganizationAuditLog } from 'data/organizations/organization-audit-logs-query' +import { useOrganizationDetailQuery } from 'data/organizations/organization-detail-query' +import dayjs from 'dayjs' +import { Button, Input, SidePanel } from 'ui' + +export interface LogDetailsPanelProps { + selectedLog?: OrganizationAuditLog + onClose: () => void +} + +const LogDetailsPanel = ({ selectedLog, onClose }: LogDetailsPanelProps) => { + console.log({ selectedLog }) + + return ( + + General}> + + + + + + + + + Actor}> + + + + + + + + + Action}> + + + + + + + + + Permission Group}> + + + + + + + ) +} + +export default LogDetailsPanel diff --git a/studio/components/interfaces/Organization/index.ts b/studio/components/interfaces/Organization/index.ts index beee07cbc43..78b4f8f97bc 100644 --- a/studio/components/interfaces/Organization/index.ts +++ b/studio/components/interfaces/Organization/index.ts @@ -3,5 +3,6 @@ import TeamSettings from './TeamSettings/TeamSettings' import BillingSettings from './BillingSettings/BillingSettings' import InvoicesSettings from './InvoicesSettings/InvoicesSettings' import NewOrgForm from './NewOrg/NewOrgForm' +import AuditLogs from './AuditLogs/AuditLogs' -export { GeneralSettings, TeamSettings, BillingSettings, InvoicesSettings, NewOrgForm } +export { GeneralSettings, TeamSettings, BillingSettings, InvoicesSettings, NewOrgForm, AuditLogs } diff --git a/studio/components/ui/AlertError.tsx b/studio/components/ui/AlertError.tsx new file mode 100644 index 00000000000..04294793831 --- /dev/null +++ b/studio/components/ui/AlertError.tsx @@ -0,0 +1,38 @@ +import Link from 'next/link' +import { Alert, Button } from 'ui' + +export interface AlertErrorProps { + ref?: string + subject?: string +} + +// [Joshen] To standardize the language for all error UIs + +const AlertError = ({ ref, subject }: AlertErrorProps) => { + const subjectString = subject?.replace(/ /g, '%20') + let href = `/support/new?category=dashboard_bug` + + if (ref) href += `&ref=${ref}` + if (subjectString) href += `&subject=${subjectString}` + + return ( + + + + + , + ]} + > + Try refreshing your browser, but if the issue persists, please reach out to us via support. + + ) +} + +export default AlertError diff --git a/studio/data/organizations/keys.ts b/studio/data/organizations/keys.ts index 72e646037bb..71baa33c8f9 100644 --- a/studio/data/organizations/keys.ts +++ b/studio/data/organizations/keys.ts @@ -7,4 +7,5 @@ export const organizationKeys = { ['organizations', slug, 'free-project-limit-check'] as const, customerProfile: (slug: string | undefined) => ['organizations', slug, 'customer-profile'] as const, + auditLogs: (slug: string | undefined) => ['organizations', slug, 'audit-logs'] as const, } diff --git a/studio/data/organizations/organization-audit-logs-query.ts b/studio/data/organizations/organization-audit-logs-query.ts new file mode 100644 index 00000000000..49f1cc6e707 --- /dev/null +++ b/studio/data/organizations/organization-audit-logs-query.ts @@ -0,0 +1,85 @@ +import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query' +import { get } from 'lib/common/fetch' +import { API_URL } from 'lib/constants' +import { useCallback } from 'react' +import { organizationKeys } from './keys' + +export type OrganizationAuditLog = { + action: { + metadata: { + method: string + status: number + }[] + name: string + } + actor: { + id: string + type: 'user' | string + metadata: { + email: string + }[] + } + permission_group: { + org_slug: string + project_ref: string | null + } + target: { + description: string + } + timestamp: string +} + +export type OrganizationAuditLogsVariables = { + slug?: string + iso_timestamp_start?: string + iso_timestamp_end?: string + project_refs?: string // Comma-separated +} + +export async function getOrganizationAuditLogs( + { slug, iso_timestamp_start, iso_timestamp_end, project_refs }: OrganizationAuditLogsVariables, + signal?: AbortSignal +) { + if (!slug) throw new Error('slug is required') + + const response = await get( + `${API_URL}/organizations/${slug}/audit?iso_timestamp_start=${iso_timestamp_start}&iso_timestamp_end=${iso_timestamp_end}`, + { signal } + ) + if (response.error) throw response.error + return response.result as OrganizationAuditLog[] +} + +export type OrganizationAuditLogsData = Awaited> +export type OrganizationAuditLogsError = unknown + +export const useOrganizationAuditLogsQuery = ( + vars: OrganizationAuditLogsVariables, + { + enabled = true, + ...options + }: UseQueryOptions = {} +) => { + const { slug } = vars + return useQuery( + organizationKeys.auditLogs(slug), + ({ signal }) => getOrganizationAuditLogs(vars, signal), + { + enabled: enabled && typeof slug !== 'undefined', + ...options, + } + ) +} + +export const useOrganizationAuditLogsPrefetch = (vars: OrganizationAuditLogsVariables) => { + const { slug } = vars + const client = useQueryClient() + + return useCallback(() => { + if (slug) { + client.prefetchQuery(organizationKeys.auditLogs(slug), ({ signal }) => + getOrganizationAuditLogs(vars, signal) + ) + } + }, [client, slug, vars]) +} diff --git a/studio/pages/org/[slug]/audit.tsx b/studio/pages/org/[slug]/audit.tsx new file mode 100644 index 00000000000..c16e6ca7cf6 --- /dev/null +++ b/studio/pages/org/[slug]/audit.tsx @@ -0,0 +1,57 @@ +import { useRouter } from 'next/router' +import { observer } from 'mobx-react-lite' +import { Tabs } from 'ui' + +import { NextPageWithLayout } from 'types' +import { useStore } from 'hooks' +import { useParams } from 'common/hooks' +import Loading from 'components/ui/Loading' +import { OrganizationLayout } from 'components/layouts' +import { AuditLogs } from 'components/interfaces/Organization' + +const OrgAuditLogs: NextPageWithLayout = () => { + const { ui } = useStore() + const { slug } = useParams() + const router = useRouter() + + return ( + <> + {ui.selectedOrganization === undefined && (ui?.permissions ?? []).length === 0 ? ( + + ) : ( +
+
+
+

+ {ui.selectedOrganization?.name ?? 'Organization'} settings +

+
+ +
+ +
+ +
+
+ )} + + ) +} + +OrgAuditLogs.getLayout = (page) => {page} +export default observer(OrgAuditLogs) From 80a0bd040c308e54e8e17c4af373d36e813e47d6 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Fri, 23 Jun 2023 16:45:39 +0800 Subject: [PATCH 02/22] Small update to Sidepanel in ui --- .../ui/src/components/SidePanel/SidePanel.tsx | 62 +++++++++---------- 1 file changed, 30 insertions(+), 32 deletions(-) diff --git a/packages/ui/src/components/SidePanel/SidePanel.tsx b/packages/ui/src/components/SidePanel/SidePanel.tsx index f5dd4fe552c..13622bbce5d 100644 --- a/packages/ui/src/components/SidePanel/SidePanel.tsx +++ b/packages/ui/src/components/SidePanel/SidePanel.tsx @@ -61,10 +61,6 @@ const SidePanel = ({ }: SidePanelProps) => { const __styles = styleHandler('sidepanel') - // function stopPropagation(e: React.MouseEvent) { - // e.stopPropagation() - // } - const footerContent = customFooter ? ( customFooter ) : ( @@ -74,35 +70,37 @@ const SidePanel = ({ {cancelText} - - -
- -
-
- {tooltip !== undefined && ( - - - -
+ +
+
- - - )} - + {confirmText} + +
+ + {tooltip !== undefined && ( + + + +
+ {tooltip} +
+
+
+ )} +
+ )} ) From 872f77ed94c2e13dd256190ae491f8af8e319f9a Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Fri, 23 Jun 2023 16:52:33 +0800 Subject: [PATCH 03/22] Tidy up --- .../Organization/AuditLogs/AuditLogs.tsx | 27 +++++++++++++------ .../AuditLogs/LogDetailsPanel.tsx | 14 +++++++++- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx b/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx index ecb23296b69..0e0b1bfdd3f 100644 --- a/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx +++ b/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx @@ -13,7 +13,7 @@ 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 { IconSearch, IconUser, Input } from 'ui' +import { Button, IconInfo, IconSearch, IconUser, Input } from 'ui' import LogDetailsPanel from './LogDetailsPanel' // [Joshen considerations] @@ -72,6 +72,7 @@ const AuditLogs = () => { Action, Target, Date, + , ]} body={ sortedLogs?.map((log) => { @@ -84,10 +85,15 @@ const AuditLogs = () => { (org) => org.slug === log.permission_group.org_slug ) + const hasStatusCode = log.action.metadata[0]?.status !== undefined const userIcon = - user?.invited_id || user?.username === user?.primary_email ? ( -
- + user === undefined ? ( +
+

?

+
+ ) : user?.invited_id || user?.username === user?.primary_email ? ( +
+
) : ( { {userIcon}

{user?.username ?? log.actor.id}

-

{role?.name}

+ {role &&

{role?.name}

}
-

- {log.action.metadata[0].status} -

+ {hasStatusCode && ( +

+ {log.action.metadata[0].status} +

+ )}

{log.action.name}

@@ -131,6 +139,9 @@ const AuditLogs = () => {

{dayjs(log.timestamp).format('DD MMM YYYY, HH:mm:ss')} + + + ) }) ?? [] diff --git a/studio/components/interfaces/Organization/AuditLogs/LogDetailsPanel.tsx b/studio/components/interfaces/Organization/AuditLogs/LogDetailsPanel.tsx index 34bbf7f9c35..c1dce20cd16 100644 --- a/studio/components/interfaces/Organization/AuditLogs/LogDetailsPanel.tsx +++ b/studio/components/interfaces/Organization/AuditLogs/LogDetailsPanel.tsx @@ -2,6 +2,8 @@ import { useParams } from 'common' import { FormSection, FormSectionContent, FormSectionLabel } from 'components/ui/Forms' import { OrganizationAuditLog } from 'data/organizations/organization-audit-logs-query' import { useOrganizationDetailQuery } from 'data/organizations/organization-detail-query' +import { useOrganizationsQuery } from 'data/organizations/organizations-query' +import { useProjectsQuery } from 'data/projects/projects-query' import dayjs from 'dayjs' import { Button, Input, SidePanel } from 'ui' @@ -11,7 +13,15 @@ export interface LogDetailsPanelProps { } const LogDetailsPanel = ({ selectedLog, onClose }: LogDetailsPanelProps) => { - console.log({ selectedLog }) + const { data: projects } = useProjectsQuery() + const { data: organizations } = useOrganizationsQuery() + + const project = projects?.find( + (project) => project.ref === selectedLog?.permission_group.project_ref + ) + const organization = organizations?.find( + (org) => org.slug === selectedLog?.permission_group.org_slug + ) return ( { label="Organization slug" disabled={(selectedLog?.permission_group.org_slug ?? '').length === 0} value={selectedLog?.permission_group.org_slug ?? 'None'} + descriptionText={organization?.name && `Organization: ${organization.name}`} /> { label="Project reference" disabled={(selectedLog?.permission_group.project_ref ?? '').length === 0} value={selectedLog?.permission_group.project_ref ?? 'None'} + descriptionText={project?.name && `Project: ${project.name}`} /> From c55882308293196f13bba75f1c23aade5a8314d3 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Fri, 23 Jun 2023 16:55:19 +0800 Subject: [PATCH 04/22] Empty state --- .../Organization/AuditLogs/AuditLogs.tsx | 191 ++++++++++-------- 1 file changed, 104 insertions(+), 87 deletions(-) diff --git a/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx b/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx index 0e0b1bfdd3f..9883b1524ad 100644 --- a/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx +++ b/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx @@ -13,8 +13,9 @@ 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 { Button, IconInfo, IconSearch, IconUser, Input } from 'ui' +import { Button, IconSearch, IconUser, Input } from 'ui' import LogDetailsPanel from './LogDetailsPanel' +import AlertError from 'components/ui/AlertError' // [Joshen considerations] // - Maybe fix the height of the table to the remaining height of the viewport, so that the search input is always visible @@ -56,97 +57,113 @@ const AuditLogs = () => { )} + {isError && } + {isSuccess && ( <> -
- } - placeholder="Search audit logs" - /> -
-
User, - Action, - Target, - Date, - , - ]} - body={ - sortedLogs?.map((log) => { - const user = members.find((member) => member.gotrue_id === log.actor.id) - const role = roles.find((role) => user?.role_ids?.[0] === role.id) - const project = projects?.find( - (project) => project.ref === log.permission_group.project_ref - ) - const organization = organizations?.find( - (org) => org.slug === log.permission_group.org_slug - ) + {logs.length === 0 ? ( +
+

+ Your organization does not have any audit logs available yet +

+
+ ) : ( + <> +
+ } + placeholder="Search audit logs" + /> +
+
User, + Action, + Target, + Date, + , + ]} + body={ + sortedLogs?.map((log) => { + const user = members.find((member) => member.gotrue_id === log.actor.id) + const role = roles.find((role) => user?.role_ids?.[0] === role.id) + const project = projects?.find( + (project) => project.ref === log.permission_group.project_ref + ) + const organization = organizations?.find( + (org) => org.slug === log.permission_group.org_slug + ) - const hasStatusCode = log.action.metadata[0]?.status !== undefined - const userIcon = - user === undefined ? ( -
-

?

-
- ) : user?.invited_id || user?.username === user?.primary_email ? ( -
- -
- ) : ( - - ) - - return ( - setSelectedLog(log)} - className="cursor-pointer hover:!bg-scale-100 transition duration-100" - > - -
- {userIcon} -
-

{user?.username ?? log.actor.id}

- {role &&

{role?.name}

} + const hasStatusCode = log.action.metadata[0]?.status !== undefined + const userIcon = + user === undefined ? ( +
+

?

-
- - -
- {hasStatusCode && ( -

- {log.action.metadata[0].status} + ) : user?.invited_id || user?.username === user?.primary_email ? ( +

+ +
+ ) : ( + + ) + + return ( + setSelectedLog(log)} + className="cursor-pointer hover:!bg-scale-100 transition duration-100" + > + +
+ {userIcon} +
+

{user?.username ?? log.actor.id}

+ {role && ( +

{role?.name}

+ )} +
+
+
+ +
+ {hasStatusCode && ( +

+ {log.action.metadata[0].status} +

+ )} +

{log.action.name}

+
+
+ +

+ {project?.name ?? organization?.name ?? 'Entity no longer exists'}

- )} -

{log.action.name}

-
-
- -

- {project?.name ?? organization?.name ?? 'Entity no longer exists'} -

-

- {log.permission_group.org_slug ?? log.permission_group.project_ref} -

-
- {dayjs(log.timestamp).format('DD MMM YYYY, HH:mm:ss')} - - - - - ) - }) ?? [] - } - /> +

+ {log.permission_group.org_slug ?? log.permission_group.project_ref} +

+ + + {dayjs(log.timestamp).format('DD MMM YYYY, HH:mm:ss')} + + + + + + ) + }) ?? [] + } + /> + + )} )}
From d0f406a0ef589142c97c80d526d07417873727a2 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Fri, 23 Jun 2023 16:58:39 +0800 Subject: [PATCH 05/22] feature flag --- studio/pages/org/[slug]/billing.tsx | 5 ++++- studio/pages/org/[slug]/general.tsx | 5 ++++- studio/pages/org/[slug]/invoices.tsx | 5 ++++- studio/pages/org/[slug]/team.tsx | 5 ++++- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/studio/pages/org/[slug]/billing.tsx b/studio/pages/org/[slug]/billing.tsx index 83c44735b40..95f77d26e51 100644 --- a/studio/pages/org/[slug]/billing.tsx +++ b/studio/pages/org/[slug]/billing.tsx @@ -3,7 +3,7 @@ import { observer } from 'mobx-react-lite' import { Tabs } from 'ui' import { NextPageWithLayout } from 'types' -import { useStore } from 'hooks' +import { useFlag, useStore } from 'hooks' import { useParams } from 'common/hooks' import Loading from 'components/ui/Loading' import { OrganizationLayout } from 'components/layouts' @@ -14,6 +14,8 @@ const OrgBillingSettings: NextPageWithLayout = () => { const { slug } = useParams() const router = useRouter() + const showAuditLogs = useFlag('auditLogs') + return ( <> {ui.selectedOrganization === undefined && (ui?.permissions ?? []).length === 0 ? ( @@ -39,6 +41,7 @@ const OrgBillingSettings: NextPageWithLayout = () => { + {showAuditLogs && } diff --git a/studio/pages/org/[slug]/general.tsx b/studio/pages/org/[slug]/general.tsx index f07415ba47e..9e082bf5fa3 100644 --- a/studio/pages/org/[slug]/general.tsx +++ b/studio/pages/org/[slug]/general.tsx @@ -3,7 +3,7 @@ import { observer } from 'mobx-react-lite' import { Tabs } from 'ui' import { NextPageWithLayout } from 'types' -import { useStore } from 'hooks' +import { useFlag, useStore } from 'hooks' import { useParams } from 'common/hooks' import Loading from 'components/ui/Loading' import { OrganizationLayout } from 'components/layouts' @@ -14,6 +14,8 @@ const OrgGeneralSettings: NextPageWithLayout = () => { const { slug } = useParams() const router = useRouter() + const showAuditLogs = useFlag('auditLogs') + return ( <> {ui.selectedOrganization === undefined && (ui?.permissions ?? []).length === 0 ? ( @@ -39,6 +41,7 @@ const OrgGeneralSettings: NextPageWithLayout = () => { + {showAuditLogs && } diff --git a/studio/pages/org/[slug]/invoices.tsx b/studio/pages/org/[slug]/invoices.tsx index 5ccf1ebc825..6089c399461 100644 --- a/studio/pages/org/[slug]/invoices.tsx +++ b/studio/pages/org/[slug]/invoices.tsx @@ -3,7 +3,7 @@ import { observer } from 'mobx-react-lite' import { Tabs } from 'ui' import { NextPageWithLayout } from 'types' -import { useStore } from 'hooks' +import { useFlag, useStore } from 'hooks' import { useParams } from 'common/hooks' import Loading from 'components/ui/Loading' import { OrganizationLayout } from 'components/layouts' @@ -14,6 +14,8 @@ const OrgInvoices: NextPageWithLayout = () => { const { slug } = useParams() const router = useRouter() + const showAuditLogs = useFlag('auditLogs') + return ( <> {ui.selectedOrganization === undefined && (ui?.permissions ?? []).length === 0 ? ( @@ -39,6 +41,7 @@ const OrgInvoices: NextPageWithLayout = () => { + {showAuditLogs && } diff --git a/studio/pages/org/[slug]/team.tsx b/studio/pages/org/[slug]/team.tsx index 7f96623cd61..611dd981309 100644 --- a/studio/pages/org/[slug]/team.tsx +++ b/studio/pages/org/[slug]/team.tsx @@ -3,7 +3,7 @@ import { observer } from 'mobx-react-lite' import { Tabs } from 'ui' import { NextPageWithLayout } from 'types' -import { useStore } from 'hooks' +import { useFlag, useStore } from 'hooks' import { useParams } from 'common/hooks' import Loading from 'components/ui/Loading' import { OrganizationLayout } from 'components/layouts' @@ -14,6 +14,8 @@ const OrgTeamSettings: NextPageWithLayout = () => { const { slug } = useParams() const router = useRouter() + const showAuditLogs = useFlag('auditLogs') + return ( <> {ui.selectedOrganization === undefined && (ui?.permissions ?? []).length === 0 ? ( @@ -39,6 +41,7 @@ const OrgTeamSettings: NextPageWithLayout = () => { + {showAuditLogs && } From 06e2d0dd11b781aaea3fa1f4ef4ad6046f0a815b Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Fri, 23 Jun 2023 17:11:28 +0800 Subject: [PATCH 06/22] Fix AlertError --- .../components/interfaces/Organization/AuditLogs/AuditLogs.tsx | 2 +- studio/components/ui/AlertError.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx b/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx index 9883b1524ad..74150eba91d 100644 --- a/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx +++ b/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx @@ -57,7 +57,7 @@ const AuditLogs = () => { )} - {isError && } + {isError && } {isSuccess && ( <> diff --git a/studio/components/ui/AlertError.tsx b/studio/components/ui/AlertError.tsx index 04294793831..23174bc0dac 100644 --- a/studio/components/ui/AlertError.tsx +++ b/studio/components/ui/AlertError.tsx @@ -19,7 +19,7 @@ const AlertError = ({ ref, subject }: AlertErrorProps) => { From 55e8651573848e83374f805b36ec11f5dad22c89 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 26 Jun 2023 19:31:14 +0800 Subject: [PATCH 07/22] Update search funcitonality for audit logs --- .../Organization/AuditLogs/AuditLogs.tsx | 71 ++++++++++--- .../Organization/AuditLogs/FilterPopover.tsx | 100 ++++++++++++++++++ .../organization-audit-logs-query.ts | 49 +++++++++ 3 files changed, 205 insertions(+), 15 deletions(-) create mode 100644 studio/components/interfaces/Organization/AuditLogs/FilterPopover.tsx diff --git a/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx b/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx index 74150eba91d..8c2801ab5b6 100644 --- a/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx +++ b/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx @@ -4,6 +4,7 @@ import { useState } from 'react' import { useParams } from 'common' import Table from 'components/to-be-cleaned/Table' +import AlertError from 'components/ui/AlertError' import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { OrganizationAuditLog, @@ -13,17 +14,22 @@ 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 { Button, IconSearch, IconUser, Input } from 'ui' +import { Button, IconUser } from 'ui' +import FilterPopover from './FilterPopover' import LogDetailsPanel from './LogDetailsPanel' -import AlertError from 'components/ui/AlertError' // [Joshen considerations] // - Maybe fix the height of the table to the remaining height of the viewport, so that the search input is always visible +// - We'll need pagination as well if the audit logs get too large, but that needs to be implemented on the API side first if possible const AuditLogs = () => { const { slug } = useParams() - const currentTime = dayjs() + const currentTime = dayjs().set('millisecond', 0) const [selectedLog, setSelectedLog] = useState() + const [filters, setFilters] = useState<{ users: string[]; projects: string[] }>({ + users: [], // gotrue_id + projects: [], // project_ref + }) const { data: projects } = useProjectsQuery() const { data: organizations } = useOrganizationsQuery() @@ -42,9 +48,22 @@ const AuditLogs = () => { const members = detailData?.members ?? [] const roles = rolesData?.roles ?? [] - const sortedLogs = logs?.sort( - (a, b) => Number(new Date(b.timestamp)) - Number(new Date(a.timestamp)) - ) + const sortedLogs = logs + ?.sort((a, b) => Number(new Date(b.timestamp)) - Number(new Date(a.timestamp))) + ?.filter((log) => { + if (filters.users.length > 0) { + return filters.users.includes(log.actor.id) + } else { + return log + } + }) + ?.filter((log) => { + if (filters.projects.length > 0) { + return filters.projects.includes(log.permission_group.project_ref || '') + } else { + return log + } + }) return ( <> @@ -69,14 +88,26 @@ const AuditLogs = () => { ) : ( <> -
- } - placeholder="Search audit logs" +
+

Filter by

+ setFilters({ ...filters, users: values })} + /> + setFilters({ ...filters, projects: values })} />
+
User, @@ -144,11 +175,21 @@ const AuditLogs = () => { -

+

+ {project?.name + ? 'Project: ' + : organization?.name + ? 'Organization: ' + : null} {project?.name ?? organization?.name ?? 'Entity no longer exists'}

-

- {log.permission_group.org_slug ?? log.permission_group.project_ref} +

+ {log.permission_group.project_ref + ? 'Ref: ' + : log.permission_group.org_slug + ? 'Slug: ' + : null} + {log.permission_group.project_ref ?? log.permission_group.org_slug}

diff --git a/studio/components/interfaces/Organization/AuditLogs/FilterPopover.tsx b/studio/components/interfaces/Organization/AuditLogs/FilterPopover.tsx new file mode 100644 index 00000000000..c38c96736e3 --- /dev/null +++ b/studio/components/interfaces/Organization/AuditLogs/FilterPopover.tsx @@ -0,0 +1,100 @@ +import { useState } from 'react' +import { Button, Checkbox, Popover } from 'ui' + +interface FilterPopoverProps { + options: any[] + activeOptions: any[] + valueKey: string + labelKey: string + name: string + onSaveFilters: (options: string[]) => void +} + +// [Joshen] Form + Checkbox.Group doesn't seem to work properly RE form state +// hence why the manual implementation here as a workaround + +const FilterPopover = ({ + options = [], + activeOptions = [], + valueKey, + labelKey, + name, + onSaveFilters, +}: FilterPopoverProps) => { + const [open, setOpen] = useState(false) + const [selectedOptions, setSelectedOptions] = useState([]) + + const formattedOptions = activeOptions.map((option) => { + const base = options.find((x) => x[valueKey] === option) + return base[labelKey] + }) + + return ( + setOpen(!open)} + header={
Select {name.toLowerCase()}
} + overlay={ + <> +
+ { + const value = event.target.value + if (selectedOptions.includes(value)) { + setSelectedOptions(selectedOptions.filter((x) => x !== value)) + } else { + setSelectedOptions(selectedOptions.concat(value)) + } + }} + options={options.map((option) => { + return { + value: option[valueKey], + label: option[labelKey], + defaultChecked: activeOptions.includes(option[valueKey]), + } + })} + > +
+
+ + +
+ + } + > + +
+ ) +} + +export default FilterPopover diff --git a/studio/data/organizations/organization-audit-logs-query.ts b/studio/data/organizations/organization-audit-logs-query.ts index 49f1cc6e707..07680d1a20f 100644 --- a/studio/data/organizations/organization-audit-logs-query.ts +++ b/studio/data/organizations/organization-audit-logs-query.ts @@ -29,6 +29,53 @@ export type OrganizationAuditLog = { timestamp: string } +const MOCK_LOGS: OrganizationAuditLog[] = [ + { + action: { name: 'Test', metadata: [{ method: 'Test', status: 200 }] }, + actor: { + id: '88687b48-6496-478b-a9e5-f0d626605a3b', + type: 'user', + metadata: [{ email: 'joshenlimek@gmail.com' }], + }, + permission_group: { org_slug: 'sfplxoawkrhwbgwzikum', project_ref: null }, + target: { description: 'Test description' }, + timestamp: new Date('2023-06-05 08:30:23').toISOString(), + }, + { + action: { name: 'Test', metadata: [{ method: 'Test', status: 200 }] }, + actor: { + id: '88687b48-6496-478b-a9e5-f0d626605a3b', + type: 'user', + metadata: [{ email: 'joshenlimek@gmail.com' }], + }, + permission_group: { org_slug: 'sfplxoawkrhwbgwzikum', project_ref: null }, + target: { description: 'Test description' }, + timestamp: new Date('2023-06-05 08:20:23').toISOString(), + }, + { + action: { name: 'Test', metadata: [{ method: 'Test', status: 200 }] }, + actor: { + id: '88687b48-6496-478b-a9e5-f0d626605a3b', + type: 'user', + metadata: [{ email: 'joshenlimek@gmail.com' }], + }, + permission_group: { org_slug: 'sfplxoawkrhwbgwzikum', project_ref: null }, + target: { description: 'Test description' }, + timestamp: new Date('2023-06-05 08:10:23').toISOString(), + }, + { + action: { name: 'Test', metadata: [{ method: 'Test', status: 200 }] }, + actor: { + id: '88687b48-6496-478b-a9e5-f0d626605a3b', + type: 'user', + metadata: [{ email: 'joshenlimek@gmail.com' }], + }, + permission_group: { org_slug: 'sfplxoawkrhwbgwzikum', project_ref: 'uipodoqangxyoawfsfer' }, + target: { description: 'Test description' }, + timestamp: new Date('2023-06-05 08:00:23').toISOString(), + }, +] + export type OrganizationAuditLogsVariables = { slug?: string iso_timestamp_start?: string @@ -42,6 +89,8 @@ export async function getOrganizationAuditLogs( ) { if (!slug) throw new Error('slug is required') + return MOCK_LOGS + const response = await get( `${API_URL}/organizations/${slug}/audit?iso_timestamp_start=${iso_timestamp_start}&iso_timestamp_end=${iso_timestamp_end}`, { signal } From c71451d9753d11b8b81dd829814e5655a8034aef Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 26 Jun 2023 19:31:33 +0800 Subject: [PATCH 08/22] update ui --- packages/ui/src/components/Checkbox/Checkbox.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ui/src/components/Checkbox/Checkbox.tsx b/packages/ui/src/components/Checkbox/Checkbox.tsx index 13707b536a9..0f325e4a18a 100644 --- a/packages/ui/src/components/Checkbox/Checkbox.tsx +++ b/packages/ui/src/components/Checkbox/Checkbox.tsx @@ -84,6 +84,7 @@ function Group({ checked={option.checked} name={option.name} description={option.description} + defaultChecked={option.defaultChecked} /> ) }) From 2092ec5202ff14dd0757c66613ba76c7c2726663 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 26 Jun 2023 21:21:49 +0800 Subject: [PATCH 09/22] Add sorting on date --- .../Organization/AuditLogs/AuditLogs.tsx | 41 +++++++++++++++---- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx b/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx index 8c2801ab5b6..b6b8dbfed92 100644 --- a/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx +++ b/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx @@ -14,7 +14,7 @@ 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 { Button, IconUser } from 'ui' +import { Button, IconArrowDown, IconArrowUp, IconBarChart, IconUser } from 'ui' import FilterPopover from './FilterPopover' import LogDetailsPanel from './LogDetailsPanel' @@ -25,6 +25,7 @@ import LogDetailsPanel from './LogDetailsPanel' const AuditLogs = () => { const { slug } = useParams() const currentTime = dayjs().set('millisecond', 0) + const [dateSortDesc, setDateSortDesc] = useState(true) const [selectedLog, setSelectedLog] = useState() const [filters, setFilters] = useState<{ users: string[]; projects: string[] }>({ users: [], // gotrue_id @@ -49,7 +50,11 @@ const AuditLogs = () => { const members = detailData?.members ?? [] const roles = rolesData?.roles ?? [] const sortedLogs = logs - ?.sort((a, b) => Number(new Date(b.timestamp)) - Number(new Date(a.timestamp))) + ?.sort((a, b) => + dateSortDesc + ? Number(new Date(b.timestamp)) - Number(new Date(a.timestamp)) + : Number(new Date(a.timestamp)) - Number(new Date(b.timestamp)) + ) ?.filter((log) => { if (filters.users.length > 0) { return filters.users.includes(log.actor.id) @@ -110,11 +115,33 @@ const AuditLogs = () => {
User, - Action, - Target, - Date, - , + + User + , + + Action + , + + Target + , + +
+

Date

+
+
, + , ]} body={ sortedLogs?.map((log) => { From 1eeebaca00a3e53b784ab659de32e2c5286f321c Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 26 Jun 2023 21:23:43 +0800 Subject: [PATCH 10/22] Add tooltip for sort --- .../Organization/AuditLogs/AuditLogs.tsx | 47 ++++++++++++++----- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx b/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx index b6b8dbfed92..17e3e632373 100644 --- a/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx +++ b/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx @@ -1,6 +1,7 @@ import dayjs from 'dayjs' import Image from 'next/image' import { useState } from 'react' +import * as Tooltip from '@radix-ui/react-tooltip' import { useParams } from 'common' import Table from 'components/to-be-cleaned/Table' @@ -127,18 +128,40 @@ const AuditLogs = () => {

Date

-
, , From 391622db9ebb55d656f8d48eecc9daa40edcc8ac Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 26 Jun 2023 22:21:30 +0800 Subject: [PATCH 11/22] Add date picker, but yet to hook up --- .../Organization/AuditLogs/AuditLogs.tsx | 55 +++++++++-- .../components/ui/DatePicker/DatePicker.tsx | 99 ++++++++++--------- 2 files changed, 103 insertions(+), 51 deletions(-) diff --git a/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx b/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx index 17e3e632373..ce88fbff6a3 100644 --- a/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx +++ b/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx @@ -15,9 +15,10 @@ 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 { Button, IconArrowDown, IconArrowUp, IconBarChart, IconUser } from 'ui' +import { Alert, Button, IconArrowDown, IconArrowUp, IconBarChart, IconUser } from 'ui' import FilterPopover from './FilterPopover' import LogDetailsPanel from './LogDetailsPanel' +import { DatePicker } from 'components/ui/DatePicker' // [Joshen considerations] // - Maybe fix the height of the table to the remaining height of the viewport, so that the search input is always visible @@ -25,12 +26,16 @@ import LogDetailsPanel from './LogDetailsPanel' const AuditLogs = () => { const { slug } = useParams() - const currentTime = dayjs().set('millisecond', 0) + const currentTime = dayjs().utc().set('millisecond', 0) const [dateSortDesc, setDateSortDesc] = useState(true) + const [dateRange, setDateRange] = useState({ + from: currentTime.subtract(1, 'day').toISOString(), + to: currentTime.toISOString(), + }) const [selectedLog, setSelectedLog] = useState() const [filters, setFilters] = useState<{ users: string[]; projects: string[] }>({ - users: [], // gotrue_id - projects: [], // project_ref + users: [], // gotrue_id[] + projects: [], // project_ref[] }) const { data: projects } = useProjectsQuery() @@ -44,8 +49,8 @@ const AuditLogs = () => { isError, } = useOrganizationAuditLogsQuery({ slug, - iso_timestamp_start: currentTime.subtract(1, 'day').toISOString(), - iso_timestamp_end: currentTime.toISOString(), + iso_timestamp_start: dateRange.from, + iso_timestamp_end: dateRange.to, }) const members = detailData?.members ?? [] @@ -82,6 +87,7 @@ const AuditLogs = () => { )} + {/* [Joshen] We'll need to also handle then, if the selected date range is out, rather than blocking all actions */} {isError && } {isSuccess && ( @@ -112,6 +118,43 @@ const AuditLogs = () => { activeOptions={filters.projects} onSaveFilters={(values) => setFilters({ ...filters, projects: values })} /> + { + 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={({ to, from }) => { + // If selected range is outside of retention policy + if (true) { + return ( + + Selected range is outside of included retention policy, as such no data + will be returned. + + ) + } + }} + />
void - to?: string - from?: string + to?: string // ISO string + from?: string // ISO string triggerButtonType?: ButtonProps['type'] triggerButtonClassName?: string triggerButtonTitle?: string + hideTime?: boolean + hideClear?: boolean renderFooter?: (args: DatePickerToFrom) => React.ReactNode | void } @@ -38,6 +40,8 @@ function _DatePicker({ triggerButtonType = 'default', triggerButtonClassName = '', triggerButtonTitle, + hideTime = false, + hideClear = false, renderFooter = () => null, }: DatePickerProps) { const [open, setOpen] = useState(false) @@ -64,6 +68,7 @@ function _DatePicker({ setAppliedEndDate(end) setEndDate(end) } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [to, from]) function handleDatePickerChange(dates: [from: Date | null, to: Date | null]) { @@ -81,15 +86,15 @@ function _DatePicker({ const payload = { from: dayjs .utc(startDate) - .second(startTime.ss) - .minute(startTime.mm) - .hour(startTime.HH) + .second(Number(startTime.ss)) + .minute(Number(startTime.mm)) + .hour(Number(startTime.HH)) .toISOString(), to: dayjs .utc(endDate || startDate) - .second(endTime.ss) - .minute(endTime.mm) - .hour(endTime.HH) + .second(Number(endTime.ss)) + .minute(Number(endTime.mm)) + .hour(Number(endTime.HH)) .toISOString(), } if (onChange) onChange(payload) @@ -114,47 +119,49 @@ function _DatePicker({ align="center" side="bottom" header={ - <> -
-
- -
-
+
+
+ +
+
- + > + +
+
+ +
-
- -
-
- + + ) } overlay={ <> @@ -213,9 +220,11 @@ function _DatePicker({ })}
- + {!hideClear && ( + + )}
From e82e8ec7d9356af14e0f328d4caea02c37ae84b9 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 27 Jun 2023 14:02:59 +0800 Subject: [PATCH 12/22] Add retention policy --- .../Organization/AuditLogs/AuditLogs.tsx | 45 +++++++++++-------- .../components/ui/DatePicker/DatePicker.tsx | 10 ++++- .../organization-audit-logs-query.ts | 9 +++- 3 files changed, 41 insertions(+), 23 deletions(-) diff --git a/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx b/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx index ce88fbff6a3..8b84b0d8965 100644 --- a/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx +++ b/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx @@ -1,11 +1,12 @@ +import * as Tooltip from '@radix-ui/react-tooltip' import dayjs from 'dayjs' import Image from 'next/image' import { useState } from 'react' -import * as Tooltip from '@radix-ui/react-tooltip' import { useParams } from 'common' import Table from 'components/to-be-cleaned/Table' import AlertError from 'components/ui/AlertError' +import { DatePicker } from 'components/ui/DatePicker' import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { OrganizationAuditLog, @@ -15,14 +16,17 @@ 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, IconBarChart, IconUser } from 'ui' +import { Alert, Button, IconArrowDown, IconArrowUp, IconUser } from 'ui' import FilterPopover from './FilterPopover' import LogDetailsPanel from './LogDetailsPanel' -import { DatePicker } from 'components/ui/DatePicker' // [Joshen considerations] // - Maybe fix the height of the table to the remaining height of the viewport, so that the search input is always visible // - We'll need pagination as well if the audit logs get too large, but that needs to be implemented on the API side first if possible +// - Need to update RQ's resource key, so that it can update with iso_timestamp_start/end changes, check Alaister OR LogsExplorer +// - Need to update to show retention policy (KM will update endpoint) +// - I've hidden time input in the date picker for now cause the time support in the component is a bit iffy, need to investigate +// - Maybe a rule to follow from here is just everytime we call dayjs, use UTC(), one TZ to rule them all const AuditLogs = () => { const { slug } = useParams() @@ -42,12 +46,7 @@ const AuditLogs = () => { const { data: organizations } = useOrganizationsQuery() const { data: detailData } = useOrganizationDetailQuery({ slug }) const { data: rolesData } = useOrganizationRolesQuery({ slug }) - const { - data: logs, - isLoading, - isSuccess, - isError, - } = useOrganizationAuditLogsQuery({ + const { data, isLoading, isSuccess, isError } = useOrganizationAuditLogsQuery({ slug, iso_timestamp_start: dateRange.from, iso_timestamp_end: dateRange.to, @@ -55,6 +54,9 @@ const AuditLogs = () => { const members = detailData?.members ?? [] const roles = rolesData?.roles ?? [] + + const retentionPeriod = data?.retention_period ?? 0 + const logs = data?.result ?? [] const sortedLogs = logs ?.sort((a, b) => dateSortDesc @@ -125,6 +127,8 @@ const AuditLogs = () => { 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() @@ -143,16 +147,19 @@ const AuditLogs = () => { setDateRange({ from, to }) } }} - renderFooter={({ to, from }) => { - // If selected range is outside of retention policy - if (true) { - return ( - - Selected range is outside of included retention policy, as such no data - will be returned. - - ) - } + renderFooter={() => { + return ( + + Your organization has a log retention period of{' '} + + {retentionPeriod} day + {retentionPeriod > 1 ? 's' : ''} + + . You may only view logs from{' '} + {dayjs().subtract(retentionPeriod, 'days').format('DD MMM YYYY')} as the + earliest date. + + ) }} />
diff --git a/studio/components/ui/DatePicker/DatePicker.tsx b/studio/components/ui/DatePicker/DatePicker.tsx index 20f8a719342..ba89eeecfe1 100644 --- a/studio/components/ui/DatePicker/DatePicker.tsx +++ b/studio/components/ui/DatePicker/DatePicker.tsx @@ -22,6 +22,8 @@ export interface DatePickerProps { triggerButtonType?: ButtonProps['type'] triggerButtonClassName?: string triggerButtonTitle?: string + minDate?: Date + maxDate?: Date hideTime?: boolean hideClear?: boolean renderFooter?: (args: DatePickerToFrom) => React.ReactNode | void @@ -40,6 +42,8 @@ function _DatePicker({ triggerButtonType = 'default', triggerButtonClassName = '', triggerButtonTitle, + minDate, + maxDate, hideTime = false, hideClear = false, renderFooter = () => null, @@ -167,6 +171,8 @@ function _DatePicker({ <>
{ handleDatePickerChange(dates) @@ -174,8 +180,8 @@ function _DatePicker({ dateFormat="MMMM d, yyyy h:mm aa" startDate={startDate} endDate={endDate} - selectsRange - inline + minDate={minDate} + maxDate={maxDate} dayClassName={() => 'cursor-pointer'} renderCustomHeader={({ date, diff --git a/studio/data/organizations/organization-audit-logs-query.ts b/studio/data/organizations/organization-audit-logs-query.ts index 07680d1a20f..14adacde7c4 100644 --- a/studio/data/organizations/organization-audit-logs-query.ts +++ b/studio/data/organizations/organization-audit-logs-query.ts @@ -29,6 +29,11 @@ export type OrganizationAuditLog = { timestamp: string } +export type OrganizationAuditLogsResponse = { + result: OrganizationAuditLog[] + retention_period: number +} + const MOCK_LOGS: OrganizationAuditLog[] = [ { action: { name: 'Test', metadata: [{ method: 'Test', status: 200 }] }, @@ -89,14 +94,14 @@ export async function getOrganizationAuditLogs( ) { if (!slug) throw new Error('slug is required') - return MOCK_LOGS + // return { result: MOCK_LOGS, retention_period: 1 } const response = await get( `${API_URL}/organizations/${slug}/audit?iso_timestamp_start=${iso_timestamp_start}&iso_timestamp_end=${iso_timestamp_end}`, { signal } ) if (response.error) throw response.error - return response.result as OrganizationAuditLog[] + return response as OrganizationAuditLogsResponse } export type OrganizationAuditLogsData = Awaited> From 50c9b46bdf7de1562f1551d48f718769ca0e021e Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 27 Jun 2023 14:26:18 +0800 Subject: [PATCH 13/22] Update RQ to consider time start and end for audit logs --- studio/data/organizations/keys.ts | 8 +++++++- .../organizations/organization-audit-logs-query.ts | 14 ++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/studio/data/organizations/keys.ts b/studio/data/organizations/keys.ts index 71baa33c8f9..3721083f488 100644 --- a/studio/data/organizations/keys.ts +++ b/studio/data/organizations/keys.ts @@ -7,5 +7,11 @@ export const organizationKeys = { ['organizations', slug, 'free-project-limit-check'] as const, customerProfile: (slug: string | undefined) => ['organizations', slug, 'customer-profile'] as const, - auditLogs: (slug: string | undefined) => ['organizations', slug, 'audit-logs'] as const, + auditLogs: ( + slug: string | undefined, + { + iso_timestamp_start, + iso_timestamp_end, + }: { iso_timestamp_start: string | undefined; iso_timestamp_end: string | undefined } + ) => ['organizations', slug, 'audit-logs', { iso_timestamp_start, iso_timestamp_end }] as const, } diff --git a/studio/data/organizations/organization-audit-logs-query.ts b/studio/data/organizations/organization-audit-logs-query.ts index 14adacde7c4..1bbe0b1d3f1 100644 --- a/studio/data/organizations/organization-audit-logs-query.ts +++ b/studio/data/organizations/organization-audit-logs-query.ts @@ -114,9 +114,9 @@ export const useOrganizationAuditLogsQuery = ...options }: UseQueryOptions = {} ) => { - const { slug } = vars + const { slug, iso_timestamp_start, iso_timestamp_end } = vars return useQuery( - organizationKeys.auditLogs(slug), + organizationKeys.auditLogs(slug, { iso_timestamp_start, iso_timestamp_end }), ({ signal }) => getOrganizationAuditLogs(vars, signal), { enabled: enabled && typeof slug !== 'undefined', @@ -126,14 +126,16 @@ export const useOrganizationAuditLogsQuery = } export const useOrganizationAuditLogsPrefetch = (vars: OrganizationAuditLogsVariables) => { - const { slug } = vars + const { slug, iso_timestamp_start, iso_timestamp_end } = vars const client = useQueryClient() return useCallback(() => { if (slug) { - client.prefetchQuery(organizationKeys.auditLogs(slug), ({ signal }) => - getOrganizationAuditLogs(vars, signal) + client.prefetchQuery( + organizationKeys.auditLogs(slug, { iso_timestamp_start, iso_timestamp_end }), + ({ signal }) => getOrganizationAuditLogs(vars, signal) ) } - }, [client, slug, vars]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [client, vars]) } From 6ca180bb05ebf6230fa20036b0fe248fef09830f Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 27 Jun 2023 14:30:55 +0800 Subject: [PATCH 14/22] Update empty state for no logs based on filters --- .../Organization/AuditLogs/AuditLogs.tsx | 401 +++++++++--------- 1 file changed, 199 insertions(+), 202 deletions(-) diff --git a/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx b/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx index 8b84b0d8965..61d80148bfd 100644 --- a/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx +++ b/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx @@ -23,8 +23,6 @@ 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 // - We'll need pagination as well if the audit logs get too large, but that needs to be implemented on the API side first if possible -// - Need to update RQ's resource key, so that it can update with iso_timestamp_start/end changes, check Alaister OR LogsExplorer -// - Need to update to show retention policy (KM will update endpoint) // - I've hidden time input in the date picker for now cause the time support in the component is a bit iffy, need to investigate // - Maybe a rule to follow from here is just everytime we call dayjs, use UTC(), one TZ to rule them all @@ -89,221 +87,220 @@ const AuditLogs = () => {
)} - {/* [Joshen] We'll need to also handle then, if the selected date range is out, rather than blocking all actions */} {isError && } {isSuccess && ( <> +
+

Filter by

+ setFilters({ ...filters, users: values })} + /> + setFilters({ ...filters, projects: values })} + /> + { + 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 ( + + Your organization has a log retention period of{' '} + + {retentionPeriod} day + {retentionPeriod > 1 ? 's' : ''} + + . You may only view logs from{' '} + {dayjs().subtract(retentionPeriod, 'days').format('DD MMM YYYY')} as the + earliest date. + + ) + }} + /> +
+ {logs.length === 0 ? (

Your organization does not have any audit logs available yet

+ ) : logs.length > 0 && sortedLogs.length === 0 ? ( +
+

No audit logs found based on the filters applied

+
) : ( - <> -
-

Filter by

- setFilters({ ...filters, users: values })} - /> - setFilters({ ...filters, projects: values })} - /> - { - 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 ( - - Your organization has a log retention period of{' '} - - {retentionPeriod} day - {retentionPeriod > 1 ? 's' : ''} - - . You may only view logs from{' '} - {dayjs().subtract(retentionPeriod, 'days').format('DD MMM YYYY')} as the - earliest date. - - ) - }} - /> -
+
+ User + , + + Action + , + + Target + , + +
+

Date

-
- User - , - - Action - , - - Target - , - -
-

Date

- - - -
-
, - , - ]} - body={ - sortedLogs?.map((log) => { - const user = members.find((member) => member.gotrue_id === log.actor.id) - const role = roles.find((role) => user?.role_ids?.[0] === role.id) - const project = projects?.find( - (project) => project.ref === log.permission_group.project_ref - ) - const organization = organizations?.find( - (org) => org.slug === log.permission_group.org_slug - ) - - const hasStatusCode = log.action.metadata[0]?.status !== undefined - const userIcon = - user === undefined ? ( -
-

?

-
- ) : user?.invited_id || user?.username === user?.primary_email ? ( -
- -
- ) : ( - + + - - + + + + + + , + , + ]} + body={ + sortedLogs?.map((log) => { + const user = members.find((member) => member.gotrue_id === log.actor.id) + const role = roles.find((role) => user?.role_ids?.[0] === role.id) + const project = projects?.find( + (project) => project.ref === log.permission_group.project_ref + ) + const organization = organizations?.find( + (org) => org.slug === log.permission_group.org_slug + ) + + const hasStatusCode = log.action.metadata[0]?.status !== undefined + const userIcon = + user === undefined ? ( +
+

?

+
+ ) : user?.invited_id || user?.username === user?.primary_email ? ( +
+ +
+ ) : ( + ) - }) ?? [] - } - /> - + + return ( + setSelectedLog(log)} + className="cursor-pointer hover:!bg-scale-100 transition duration-100" + > + +
+ {userIcon} +
+

{user?.username ?? log.actor.id}

+ {role && ( +

{role?.name}

+ )} +
+
+
+ +
+ {hasStatusCode && ( +

+ {log.action.metadata[0].status} +

+ )} +

{log.action.name}

+
+
+ +

+ {project?.name + ? 'Project: ' + : organization?.name + ? 'Organization: ' + : null} + {project?.name ?? organization?.name ?? 'Entity no longer exists'} +

+

+ {log.permission_group.project_ref + ? 'Ref: ' + : log.permission_group.org_slug + ? 'Slug: ' + : null} + {log.permission_group.project_ref ?? log.permission_group.org_slug} +

+
+ {dayjs(log.timestamp).format('DD MMM YYYY, HH:mm:ss')} + + + +
+ ) + }) ?? [] + } + /> )} )} From 9b1d295b3f5f26ed8cff8775defcc32b2e59f423 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 27 Jun 2023 14:37:05 +0800 Subject: [PATCH 15/22] Add log count --- .../components/interfaces/Organization/AuditLogs/AuditLogs.tsx | 2 ++ studio/data/organizations/organization-audit-logs-query.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx b/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx index 61d80148bfd..6c76ca5a593 100644 --- a/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx +++ b/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx @@ -151,6 +151,8 @@ const AuditLogs = () => { ) }} /> +
+

Viewing {sortedLogs.length} logs in total

{logs.length === 0 ? ( diff --git a/studio/data/organizations/organization-audit-logs-query.ts b/studio/data/organizations/organization-audit-logs-query.ts index 1bbe0b1d3f1..0ce7599d018 100644 --- a/studio/data/organizations/organization-audit-logs-query.ts +++ b/studio/data/organizations/organization-audit-logs-query.ts @@ -94,7 +94,7 @@ export async function getOrganizationAuditLogs( ) { if (!slug) throw new Error('slug is required') - // return { result: MOCK_LOGS, retention_period: 1 } + return { result: MOCK_LOGS, retention_period: 1 } const response = await get( `${API_URL}/organizations/${slug}/audit?iso_timestamp_start=${iso_timestamp_start}&iso_timestamp_end=${iso_timestamp_end}`, From 171a40475a6e099e4b8921063bb9172e9bca375d Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 27 Jun 2023 14:44:01 +0800 Subject: [PATCH 16/22] Remove mock --- studio/data/organizations/organization-audit-logs-query.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/studio/data/organizations/organization-audit-logs-query.ts b/studio/data/organizations/organization-audit-logs-query.ts index 0ce7599d018..1bbe0b1d3f1 100644 --- a/studio/data/organizations/organization-audit-logs-query.ts +++ b/studio/data/organizations/organization-audit-logs-query.ts @@ -94,7 +94,7 @@ export async function getOrganizationAuditLogs( ) { if (!slug) throw new Error('slug is required') - return { result: MOCK_LOGS, retention_period: 1 } + // return { result: MOCK_LOGS, retention_period: 1 } const response = await get( `${API_URL}/organizations/${slug}/audit?iso_timestamp_start=${iso_timestamp_start}&iso_timestamp_end=${iso_timestamp_end}`, From 130557c6e675777848a4f203a3c5a7d30759db7b Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 3 Jul 2023 17:16:34 +0800 Subject: [PATCH 17/22] fiux --- studio/pages/org/[slug]/general.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/studio/pages/org/[slug]/general.tsx b/studio/pages/org/[slug]/general.tsx index d3641109751..4fbffa0bd93 100644 --- a/studio/pages/org/[slug]/general.tsx +++ b/studio/pages/org/[slug]/general.tsx @@ -41,7 +41,7 @@ const OrgGeneralSettings: NextPageWithLayout = () => { - \ + {showOAuthApps && } {showAuditLogs && } From 11cdc82aaa50b82355a410e33193c59814ce7df6 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 3 Jul 2023 18:13:24 +0800 Subject: [PATCH 18/22] Update based on endpoint changes --- .../Organization/AuditLogs/AuditLogs.tsx | 29 +++++---- .../AuditLogs/LogDetailsPanel.tsx | 51 ++++++--------- .../organization-audit-logs-query.ts | 63 +++---------------- studio/styles/main.scss | 7 +++ 4 files changed, 52 insertions(+), 98 deletions(-) diff --git a/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx b/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx index 2dbdee07953..087d2620c68 100644 --- a/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx +++ b/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx @@ -59,8 +59,8 @@ const AuditLogs = () => { const sortedLogs = logs ?.sort((a, b) => dateSortDesc - ? Number(new Date(b.timestamp)) - Number(new Date(a.timestamp)) - : Number(new Date(a.timestamp)) - Number(new Date(b.timestamp)) + ? Number(new Date(b.occurred_at)) - Number(new Date(a.occurred_at)) + : Number(new Date(a.occurred_at)) - Number(new Date(b.occurred_at)) ) ?.filter((log) => { if (filters.users.length > 0) { @@ -71,7 +71,7 @@ const AuditLogs = () => { }) ?.filter((log) => { if (filters.projects.length > 0) { - return filters.projects.includes(log.permission_group.project_ref || '') + return filters.projects.includes(log.target.metadata.project_ref || '') } else { return log } @@ -225,10 +225,10 @@ const AuditLogs = () => { const user = members.find((member) => member.gotrue_id === log.actor.id) const role = roles.find((role) => user?.role_ids?.[0] === role.id) const project = projects?.find( - (project) => project.ref === log.permission_group.project_ref + (project) => project.ref === log.target.metadata.project_ref ) const organization = organizations?.find( - (org) => org.slug === log.permission_group.org_slug + (org) => org.slug === log.target.metadata.org_slug ) const hasStatusCode = log.action.metadata[0]?.status !== undefined @@ -253,18 +253,23 @@ const AuditLogs = () => { return ( setSelectedLog(log)} className="cursor-pointer hover:!bg-scale-100 transition duration-100" >
- {userIcon} +
{userIcon}
-

{user?.username ?? log.actor.id}

+

{user?.username ?? 'Unknown'}

{role && (

{role?.name}

)} + {user === undefined && role === undefined && ( +

+ ID: {log.actor.id} +

+ )}
@@ -288,16 +293,16 @@ const AuditLogs = () => { {project?.name ?? organization?.name ?? 'Entity no longer exists'}

- {log.permission_group.project_ref + {log.target.metadata.project_ref ? 'Ref: ' - : log.permission_group.org_slug + : log.target.metadata.org_slug ? 'Slug: ' : null} - {log.permission_group.project_ref ?? log.permission_group.org_slug} + {log.target.metadata.project_ref ?? log.target.metadata.org_slug}

- {dayjs(log.timestamp).format('DD MMM YYYY, HH:mm:ss')} + {dayjs(log.occurred_at).format('DD MMM YYYY, HH:mm:ss')} diff --git a/studio/components/interfaces/Organization/AuditLogs/LogDetailsPanel.tsx b/studio/components/interfaces/Organization/AuditLogs/LogDetailsPanel.tsx index c1dce20cd16..0f374319b15 100644 --- a/studio/components/interfaces/Organization/AuditLogs/LogDetailsPanel.tsx +++ b/studio/components/interfaces/Organization/AuditLogs/LogDetailsPanel.tsx @@ -1,11 +1,9 @@ -import { useParams } from 'common' import { FormSection, FormSectionContent, FormSectionLabel } from 'components/ui/Forms' import { OrganizationAuditLog } from 'data/organizations/organization-audit-logs-query' -import { useOrganizationDetailQuery } from 'data/organizations/organization-detail-query' import { useOrganizationsQuery } from 'data/organizations/organizations-query' import { useProjectsQuery } from 'data/projects/projects-query' import dayjs from 'dayjs' -import { Button, Input, SidePanel } from 'ui' +import { Input, SidePanel } from 'ui' export interface LogDetailsPanelProps { selectedLog?: OrganizationAuditLog @@ -17,16 +15,16 @@ const LogDetailsPanel = ({ selectedLog, onClose }: LogDetailsPanelProps) => { const { data: organizations } = useOrganizationsQuery() const project = projects?.find( - (project) => project.ref === selectedLog?.permission_group.project_ref + (project) => project.ref === selectedLog?.target.metadata.project_ref ) const organization = organizations?.find( - (org) => org.slug === selectedLog?.permission_group.org_slug + (org) => org.slug === selectedLog?.target.metadata.org_slug ) return ( { - @@ -60,7 +52,9 @@ const LogDetailsPanel = ({ selectedLog, onClose }: LogDetailsPanelProps) => { readOnly size="small" label="Metadata" - value={JSON.stringify(selectedLog?.actor.metadata)} + rows={5} + className="input-mono input-xs" + value={JSON.stringify(selectedLog?.actor.metadata, null, 2)} /> @@ -74,30 +68,25 @@ const LogDetailsPanel = ({ selectedLog, onClose }: LogDetailsPanelProps) => { readOnly size="small" label="Metadata" - value={JSON.stringify(selectedLog?.action.metadata)} + rows={5} + className="input-mono input-xs" + value={JSON.stringify(selectedLog?.action.metadata, null, 2)} /> - Permission Group}> + Target}> - + - diff --git a/studio/data/organizations/organization-audit-logs-query.ts b/studio/data/organizations/organization-audit-logs-query.ts index 1bbe0b1d3f1..8179f9acf0f 100644 --- a/studio/data/organizations/organization-audit-logs-query.ts +++ b/studio/data/organizations/organization-audit-logs-query.ts @@ -7,8 +7,8 @@ import { organizationKeys } from './keys' export type OrganizationAuditLog = { action: { metadata: { - method: string - status: number + method?: string + status?: number }[] name: string } @@ -16,17 +16,17 @@ export type OrganizationAuditLog = { id: string type: 'user' | string metadata: { - email: string + email?: string }[] } - permission_group: { - org_slug: string - project_ref: string | null - } target: { description: string + metadata: { + org_slug?: string + project_ref?: string + } } - timestamp: string + occurred_at: string } export type OrganizationAuditLogsResponse = { @@ -34,53 +34,6 @@ export type OrganizationAuditLogsResponse = { retention_period: number } -const MOCK_LOGS: OrganizationAuditLog[] = [ - { - action: { name: 'Test', metadata: [{ method: 'Test', status: 200 }] }, - actor: { - id: '88687b48-6496-478b-a9e5-f0d626605a3b', - type: 'user', - metadata: [{ email: 'joshenlimek@gmail.com' }], - }, - permission_group: { org_slug: 'sfplxoawkrhwbgwzikum', project_ref: null }, - target: { description: 'Test description' }, - timestamp: new Date('2023-06-05 08:30:23').toISOString(), - }, - { - action: { name: 'Test', metadata: [{ method: 'Test', status: 200 }] }, - actor: { - id: '88687b48-6496-478b-a9e5-f0d626605a3b', - type: 'user', - metadata: [{ email: 'joshenlimek@gmail.com' }], - }, - permission_group: { org_slug: 'sfplxoawkrhwbgwzikum', project_ref: null }, - target: { description: 'Test description' }, - timestamp: new Date('2023-06-05 08:20:23').toISOString(), - }, - { - action: { name: 'Test', metadata: [{ method: 'Test', status: 200 }] }, - actor: { - id: '88687b48-6496-478b-a9e5-f0d626605a3b', - type: 'user', - metadata: [{ email: 'joshenlimek@gmail.com' }], - }, - permission_group: { org_slug: 'sfplxoawkrhwbgwzikum', project_ref: null }, - target: { description: 'Test description' }, - timestamp: new Date('2023-06-05 08:10:23').toISOString(), - }, - { - action: { name: 'Test', metadata: [{ method: 'Test', status: 200 }] }, - actor: { - id: '88687b48-6496-478b-a9e5-f0d626605a3b', - type: 'user', - metadata: [{ email: 'joshenlimek@gmail.com' }], - }, - permission_group: { org_slug: 'sfplxoawkrhwbgwzikum', project_ref: 'uipodoqangxyoawfsfer' }, - target: { description: 'Test description' }, - timestamp: new Date('2023-06-05 08:00:23').toISOString(), - }, -] - export type OrganizationAuditLogsVariables = { slug?: string iso_timestamp_start?: string diff --git a/studio/styles/main.scss b/studio/styles/main.scss index 70970273ab4..344bc70f909 100644 --- a/studio/styles/main.scss +++ b/studio/styles/main.scss @@ -150,6 +150,13 @@ a { } } +.input-xs { + input, + textarea { + @apply text-xs; + } +} + input[type='radio'] { @apply p-0; } From faa5a28a4bb270d84b14ef8e3a5474ad16f75c50 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 4 Jul 2023 12:17:09 +0800 Subject: [PATCH 19/22] Small update --- .../Organization/AuditLogs/AuditLogs.tsx | 180 ++++++++++-------- .../organization-audit-logs-query.ts | 5 +- 2 files changed, 102 insertions(+), 83 deletions(-) diff --git a/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx b/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx index 087d2620c68..0657951a511 100644 --- a/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx +++ b/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx @@ -16,7 +16,7 @@ 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, IconUser } from 'ui' +import { Alert, Button, IconArrowDown, IconArrowUp, IconRefreshCw, IconUser } from 'ui' import FilterPopover from './FilterPopover' import LogDetailsPanel from './LogDetailsPanel' import { ScaffoldContainerLegacy } from 'components/layouts/Scaffold' @@ -45,11 +45,12 @@ const AuditLogs = () => { const { data: organizations } = useOrganizationsQuery() const { data: detailData } = useOrganizationDetailQuery({ slug }) const { data: rolesData } = useOrganizationRolesQuery({ slug }) - const { data, isLoading, isSuccess, isError } = useOrganizationAuditLogsQuery({ - slug, - iso_timestamp_start: dateRange.from, - iso_timestamp_end: dateRange.to, - }) + const { data, error, isLoading, isSuccess, isError, isRefetching, refetch } = + useOrganizationAuditLogsQuery({ + slug, + iso_timestamp_start: dateRange.from, + iso_timestamp_end: dateRange.to, + }) const members = detailData?.members ?? [] const roles = rolesData?.roles ?? [] @@ -89,72 +90,82 @@ const AuditLogs = () => { )} - {isError && } + {isError && } {isSuccess && ( <> -
-

Filter by

- setFilters({ ...filters, users: values })} - /> - setFilters({ ...filters, projects: values })} - /> - { - 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 ( - - Your organization has a log retention period of{' '} - - {retentionPeriod} day - {retentionPeriod > 1 ? 's' : ''} - - . You may only view logs from{' '} - {dayjs().subtract(retentionPeriod, 'days').format('DD MMM YYYY')} as the - earliest date. - - ) - }} - /> -
-

Viewing {sortedLogs.length} logs in total

+
+
+

Filter by

+ setFilters({ ...filters, users: values })} + /> + setFilters({ ...filters, projects: values })} + /> + { + 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 ( + + Your organization has a log retention period of{' '} + + {retentionPeriod} day + {retentionPeriod > 1 ? 's' : ''} + + . You may only view logs from{' '} + {dayjs().subtract(retentionPeriod, 'days').format('DD MMM YYYY')} as the + earliest date. + + ) + }} + /> +
+

Viewing {sortedLogs.length} logs in total

+
+
{logs.length === 0 ? ( @@ -234,19 +245,19 @@ const AuditLogs = () => { const hasStatusCode = log.action.metadata[0]?.status !== undefined const userIcon = user === undefined ? ( -
+

?

) : user?.invited_id || user?.username === user?.primary_email ? ( -
+
) : ( ) @@ -265,11 +276,6 @@ const AuditLogs = () => { {role && (

{role?.name}

)} - {user === undefined && role === undefined && ( -

- ID: {log.actor.id} -

- )}
@@ -280,19 +286,29 @@ const AuditLogs = () => { {log.action.metadata[0].status}

)} -

{log.action.name}

+

+ {log.action.name} +

-

+

{project?.name ? 'Project: ' : organization?.name ? 'Organization: ' : null} - {project?.name ?? organization?.name ?? 'Entity no longer exists'} + {project?.name ?? organization?.name ?? 'Unknown'}

-

+

{log.target.metadata.project_ref ? 'Ref: ' : log.target.metadata.org_slug diff --git a/studio/data/organizations/organization-audit-logs-query.ts b/studio/data/organizations/organization-audit-logs-query.ts index 8179f9acf0f..38e21630eca 100644 --- a/studio/data/organizations/organization-audit-logs-query.ts +++ b/studio/data/organizations/organization-audit-logs-query.ts @@ -3,6 +3,7 @@ import { get } from 'lib/common/fetch' import { API_URL } from 'lib/constants' import { useCallback } from 'react' import { organizationKeys } from './keys' +import { ResponseError } from 'types' export type OrganizationAuditLog = { action: { @@ -58,7 +59,7 @@ export async function getOrganizationAuditLogs( } export type OrganizationAuditLogsData = Awaited> -export type OrganizationAuditLogsError = unknown +export type OrganizationAuditLogsError = ResponseError export const useOrganizationAuditLogsQuery = ( vars: OrganizationAuditLogsVariables, @@ -73,6 +74,8 @@ export const useOrganizationAuditLogsQuery = ({ signal }) => getOrganizationAuditLogs(vars, signal), { enabled: enabled && typeof slug !== 'undefined', + staleTime: Infinity, + cacheTime: Infinity, ...options, } ) From 2ccb94552ce72e408e1900ea4f899ef4f12e89b9 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 4 Jul 2023 12:34:16 +0800 Subject: [PATCH 20/22] Remove iso timestamp start and end from audit log resource key --- studio/data/organizations/keys.ts | 8 +------- .../organizations/organization-audit-logs-query.ts | 11 +++++------ 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/studio/data/organizations/keys.ts b/studio/data/organizations/keys.ts index ea3596a0aaf..82588884d4e 100644 --- a/studio/data/organizations/keys.ts +++ b/studio/data/organizations/keys.ts @@ -7,13 +7,7 @@ export const organizationKeys = { ['organizations', slug, 'free-project-limit-check'] as const, customerProfile: (slug: string | undefined) => ['organizations', slug, 'customer-profile'] as const, - auditLogs: ( - slug: string | undefined, - { - iso_timestamp_start, - iso_timestamp_end, - }: { iso_timestamp_start: string | undefined; iso_timestamp_end: string | undefined } - ) => ['organizations', slug, 'audit-logs', { iso_timestamp_start, iso_timestamp_end }] as const, + auditLogs: (slug: string | undefined) => ['organizations', slug, 'audit-logs'] as const, migrateBilling: (slug: string | undefined) => ['organizations', slug, 'migrate-billing'] as const, migrateBillingPreview: (slug: string | undefined) => ['organizations', slug, 'migrate-billing', 'preview'] as const, diff --git a/studio/data/organizations/organization-audit-logs-query.ts b/studio/data/organizations/organization-audit-logs-query.ts index 38e21630eca..e70c3c73756 100644 --- a/studio/data/organizations/organization-audit-logs-query.ts +++ b/studio/data/organizations/organization-audit-logs-query.ts @@ -68,9 +68,9 @@ export const useOrganizationAuditLogsQuery = ...options }: UseQueryOptions = {} ) => { - const { slug, iso_timestamp_start, iso_timestamp_end } = vars + const { slug } = vars return useQuery( - organizationKeys.auditLogs(slug, { iso_timestamp_start, iso_timestamp_end }), + organizationKeys.auditLogs(slug), ({ signal }) => getOrganizationAuditLogs(vars, signal), { enabled: enabled && typeof slug !== 'undefined', @@ -82,14 +82,13 @@ export const useOrganizationAuditLogsQuery = } export const useOrganizationAuditLogsPrefetch = (vars: OrganizationAuditLogsVariables) => { - const { slug, iso_timestamp_start, iso_timestamp_end } = vars + const { slug } = vars const client = useQueryClient() return useCallback(() => { if (slug) { - client.prefetchQuery( - organizationKeys.auditLogs(slug, { iso_timestamp_start, iso_timestamp_end }), - ({ signal }) => getOrganizationAuditLogs(vars, signal) + client.prefetchQuery(organizationKeys.auditLogs(slug), ({ signal }) => + getOrganizationAuditLogs(vars, signal) ) } // eslint-disable-next-line react-hooks/exhaustive-deps From c742ea8c7356007e98c62908a2296045544e19b6 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 4 Jul 2023 14:05:19 +0800 Subject: [PATCH 21/22] Fix --- .../Organization/AuditLogs/AuditLogs.tsx | 152 +++++++++--------- studio/data/organizations/keys.ts | 8 +- .../organization-audit-logs-query.ts | 17 +- 3 files changed, 95 insertions(+), 82 deletions(-) diff --git a/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx b/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx index 0657951a511..c463ce98397 100644 --- a/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx +++ b/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx @@ -82,6 +82,84 @@ const AuditLogs = () => { <>

+
+
+

Filter by

+ setFilters({ ...filters, users: values })} + /> + setFilters({ ...filters, projects: values })} + /> + { + 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 ( + + Your organization has a log retention period of{' '} + + {retentionPeriod} day + {retentionPeriod > 1 ? 's' : ''} + + . You may only view logs from{' '} + {dayjs().subtract(retentionPeriod, 'days').format('DD MMM YYYY')} as the + earliest date. + + ) + }} + /> + {isSuccess && ( + <> +
+

Viewing {sortedLogs.length} logs in total

+ + )} +
+ +
+ {isLoading && (
@@ -94,80 +172,6 @@ const AuditLogs = () => { {isSuccess && ( <> -
-
-

Filter by

- setFilters({ ...filters, users: values })} - /> - setFilters({ ...filters, projects: values })} - /> - { - 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 ( - - Your organization has a log retention period of{' '} - - {retentionPeriod} day - {retentionPeriod > 1 ? 's' : ''} - - . You may only view logs from{' '} - {dayjs().subtract(retentionPeriod, 'days').format('DD MMM YYYY')} as the - earliest date. - - ) - }} - /> -
-

Viewing {sortedLogs.length} logs in total

-
- -
- {logs.length === 0 ? (

diff --git a/studio/data/organizations/keys.ts b/studio/data/organizations/keys.ts index 82588884d4e..ea3596a0aaf 100644 --- a/studio/data/organizations/keys.ts +++ b/studio/data/organizations/keys.ts @@ -7,7 +7,13 @@ export const organizationKeys = { ['organizations', slug, 'free-project-limit-check'] as const, customerProfile: (slug: string | undefined) => ['organizations', slug, 'customer-profile'] as const, - auditLogs: (slug: string | undefined) => ['organizations', slug, 'audit-logs'] as const, + auditLogs: ( + slug: string | undefined, + { + iso_timestamp_start, + iso_timestamp_end, + }: { iso_timestamp_start: string | undefined; iso_timestamp_end: string | undefined } + ) => ['organizations', slug, 'audit-logs', { iso_timestamp_start, iso_timestamp_end }] as const, migrateBilling: (slug: string | undefined) => ['organizations', slug, 'migrate-billing'] as const, migrateBillingPreview: (slug: string | undefined) => ['organizations', slug, 'migrate-billing', 'preview'] as const, diff --git a/studio/data/organizations/organization-audit-logs-query.ts b/studio/data/organizations/organization-audit-logs-query.ts index e70c3c73756..6884928ea9f 100644 --- a/studio/data/organizations/organization-audit-logs-query.ts +++ b/studio/data/organizations/organization-audit-logs-query.ts @@ -48,12 +48,14 @@ export async function getOrganizationAuditLogs( ) { if (!slug) throw new Error('slug is required') - // return { result: MOCK_LOGS, retention_period: 1 } - const response = await get( `${API_URL}/organizations/${slug}/audit?iso_timestamp_start=${iso_timestamp_start}&iso_timestamp_end=${iso_timestamp_end}`, { signal } ) + // if (response.error) { + // if (response.error.code === 400 && response.error.message.includes('Date range specified exceeds the retention period')) + // throw response.error + // } if (response.error) throw response.error return response as OrganizationAuditLogsResponse } @@ -68,9 +70,9 @@ export const useOrganizationAuditLogsQuery = ...options }: UseQueryOptions = {} ) => { - const { slug } = vars + const { slug, iso_timestamp_start, iso_timestamp_end } = vars return useQuery( - organizationKeys.auditLogs(slug), + organizationKeys.auditLogs(slug, { iso_timestamp_start, iso_timestamp_end }), ({ signal }) => getOrganizationAuditLogs(vars, signal), { enabled: enabled && typeof slug !== 'undefined', @@ -82,13 +84,14 @@ export const useOrganizationAuditLogsQuery = } export const useOrganizationAuditLogsPrefetch = (vars: OrganizationAuditLogsVariables) => { - const { slug } = vars + const { slug, iso_timestamp_start, iso_timestamp_end } = vars const client = useQueryClient() return useCallback(() => { if (slug) { - client.prefetchQuery(organizationKeys.auditLogs(slug), ({ signal }) => - getOrganizationAuditLogs(vars, signal) + client.prefetchQuery( + organizationKeys.auditLogs(slug, { iso_timestamp_start, iso_timestamp_end }), + ({ signal }) => getOrganizationAuditLogs(vars, signal) ) } // eslint-disable-next-line react-hooks/exhaustive-deps From 99accb50efed1d0d712c16a114f9310ad1bf76bf Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 4 Jul 2023 14:50:18 +0800 Subject: [PATCH 22/22] fix --- studio/data/organizations/keys.ts | 7 ++----- .../organization-audit-logs-query.ts | 16 ++++++++-------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/studio/data/organizations/keys.ts b/studio/data/organizations/keys.ts index ea3596a0aaf..a1c97b3ac4f 100644 --- a/studio/data/organizations/keys.ts +++ b/studio/data/organizations/keys.ts @@ -9,11 +9,8 @@ export const organizationKeys = { ['organizations', slug, 'customer-profile'] as const, auditLogs: ( slug: string | undefined, - { - iso_timestamp_start, - iso_timestamp_end, - }: { iso_timestamp_start: string | undefined; iso_timestamp_end: string | undefined } - ) => ['organizations', slug, 'audit-logs', { iso_timestamp_start, iso_timestamp_end }] as const, + { date_start, date_end }: { date_start: string | undefined; date_end: string | undefined } + ) => ['organizations', slug, 'audit-logs', { date_start, date_end }] as const, migrateBilling: (slug: string | undefined) => ['organizations', slug, 'migrate-billing'] as const, migrateBillingPreview: (slug: string | undefined) => ['organizations', slug, 'migrate-billing', 'preview'] as const, diff --git a/studio/data/organizations/organization-audit-logs-query.ts b/studio/data/organizations/organization-audit-logs-query.ts index 6884928ea9f..8b5663dd6c2 100644 --- a/studio/data/organizations/organization-audit-logs-query.ts +++ b/studio/data/organizations/organization-audit-logs-query.ts @@ -4,6 +4,7 @@ import { API_URL } from 'lib/constants' import { useCallback } from 'react' import { organizationKeys } from './keys' import { ResponseError } from 'types' +import dayjs from 'dayjs' export type OrganizationAuditLog = { action: { @@ -52,10 +53,6 @@ export async function getOrganizationAuditLogs( `${API_URL}/organizations/${slug}/audit?iso_timestamp_start=${iso_timestamp_start}&iso_timestamp_end=${iso_timestamp_end}`, { signal } ) - // if (response.error) { - // if (response.error.code === 400 && response.error.message.includes('Date range specified exceeds the retention period')) - // throw response.error - // } if (response.error) throw response.error return response as OrganizationAuditLogsResponse } @@ -71,13 +68,14 @@ export const useOrganizationAuditLogsQuery = }: UseQueryOptions = {} ) => { const { slug, iso_timestamp_start, iso_timestamp_end } = vars + const date_start = dayjs(iso_timestamp_start).utc().format('YYYY-MM-DD') + const date_end = dayjs(iso_timestamp_end).utc().format('YYYY-MM-DD') + return useQuery( - organizationKeys.auditLogs(slug, { iso_timestamp_start, iso_timestamp_end }), + organizationKeys.auditLogs(slug, { date_start, date_end }), ({ signal }) => getOrganizationAuditLogs(vars, signal), { enabled: enabled && typeof slug !== 'undefined', - staleTime: Infinity, - cacheTime: Infinity, ...options, } ) @@ -89,8 +87,10 @@ export const useOrganizationAuditLogsPrefetch = (vars: OrganizationAuditLogsVari return useCallback(() => { if (slug) { + const date_start = dayjs(iso_timestamp_start).utc().format('YYYY-MM-DD') + const date_end = dayjs(iso_timestamp_end).utc().format('YYYY-MM-DD') client.prefetchQuery( - organizationKeys.auditLogs(slug, { iso_timestamp_start, iso_timestamp_end }), + organizationKeys.auditLogs(slug, { date_start, date_end }), ({ signal }) => getOrganizationAuditLogs(vars, signal) ) }