Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: add cluster audit logs view to kubernetes dashboard #1861

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion assets/src/components/kubernetes/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
STORAGE_REL_PATH,
WORKLOADS_REL_PATH,
getKubernetesAbsPath,
AUDIT_REL_PATH,
} from '../../routes/kubernetesRoutesConsts'
import { ResponsiveLayoutPage } from '../utils/layout/ResponsiveLayoutPage'
import { ResponsiveLayoutSidenavContainer } from '../utils/layout/ResponsiveLayoutSidenavContainer'
Expand All @@ -44,6 +45,7 @@ const directory: Directory = [
{ path: RBAC_REL_PATH, label: 'RBAC' },
{ path: CLUSTER_REL_PATH, label: 'Cluster' },
{ path: CUSTOM_RESOURCES_REL_PATH, label: 'Custom resources' },
{ path: AUDIT_REL_PATH, label: 'Audit logs' },
] as const

export default function Navigation() {
Expand All @@ -64,6 +66,8 @@ export default function Navigation() {
const pageHeaderContext = useMemo(() => ({ setHeaderContent }), [])

useLayoutEffect(() => {
dataSelect.setEnabled(true)

if (clusterId) sessionStorage.setItem(LAST_SELECTED_CLUSTER_KEY, clusterId)

const newParams = new URLSearchParams()
Expand Down Expand Up @@ -125,7 +129,7 @@ export default function Navigation() {
}}
>
<div css={{ flex: 1, overflow: 'hidden' }}>{headerContent}</div>
<DataSelectInputs dataSelect={dataSelect} />
{dataSelect.enabled && <DataSelectInputs dataSelect={dataSelect} />}
</div>
<PageHeaderContext.Provider value={pageHeaderContext}>
<DataSelect.Provider value={dataSelect}>
Expand Down
95 changes: 95 additions & 0 deletions assets/src/components/kubernetes/audit/Audit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { Table } from '@pluralsh/design-system'
import { CellContext, createColumnHelper } from '@tanstack/react-table'
import { useEffect, useMemo } from 'react'
import {
ClusterAuditLog,
useKubernetesClusterAuditLogsQuery,
} from '../../../generated/graphql.ts'
import { GqlError } from '../../utils/Alert.tsx'
import { ScrollablePage } from '../../utils/layout/ScrollablePage.tsx'
import { DateTimeCol } from '../../utils/table/DateTimeCol.tsx'
import { useFetchPaginatedData } from '../../utils/table/useFetchPaginatedData.tsx'
import UserInfo from '../../utils/UserInfo.tsx'
import { useCluster } from '../Cluster.tsx'
import { useDataSelect } from '../common/DataSelect.tsx'

const columnHelper = createColumnHelper<ClusterAuditLog>()

const RawColumn = ({ getValue }: CellContext<any, string>): string => getValue()

const pathColumn = columnHelper.accessor((log) => log?.path, {
id: 'path',
header: 'Path',
enableSorting: true,
cell: RawColumn,
})

const methodColumn = columnHelper.accessor((log) => log?.method, {
id: 'method',
header: 'Method',
enableSorting: true,
cell: RawColumn,
})

const insertedAtColumn = columnHelper.accessor((log) => log?.insertedAt, {
id: 'timestamp',
header: 'Timestamp',
enableSorting: true,
cell: ({ getValue }) => <DateTimeCol date={getValue()} />,
})

const userColumn = columnHelper.accessor((log) => log?.actor, {
id: 'user',
header: 'User',
enableSorting: true,
cell: ({ getValue }) => <UserInfo user={getValue() ?? {}} />,
})

const columns = [methodColumn, pathColumn, userColumn, insertedAtColumn]

export default function Audit() {
const cluster = useCluster()
const { setEnabled } = useDataSelect()

const { data, loading, error, pageInfo, fetchNextPage, setVirtualSlice } =
useFetchPaginatedData(
{
skip: !cluster?.id,
queryHook: useKubernetesClusterAuditLogsQuery,
keyPath: ['auditLogs'],
pollInterval: 30_000,
},
{
clusterId: cluster?.id,
}
)

useEffect(() => {
// Disable data select on audit page
setEnabled(false)
})

const auditLogs = useMemo(
() => data?.cluster?.auditLogs?.edges?.map((edge) => edge?.node),
[data]
)

return (
<ScrollablePage
fullWidth
scrollable={false}
>
{error && <GqlError error={error} />}
<Table
fullHeightWrap
data={auditLogs ?? []}
columns={columns}
hasNextPage={pageInfo?.hasNextPage}
fetchNextPage={fetchNextPage}
isFetchingNextPage={loading}
virtualizeRows
onVirtualSliceChange={setVirtualSlice}
/>
</ScrollablePage>
)
}
7 changes: 7 additions & 0 deletions assets/src/components/kubernetes/common/DataSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export type DataSelectT = {
}

export type DataSelectContextT = {
enabled: boolean
setEnabled: Dispatch<SetStateAction<boolean>>
namespaced: boolean
setNamespaced: Dispatch<SetStateAction<boolean>>
namespace: string
Expand All @@ -41,10 +43,13 @@ export function useDataSelect(defaults?: DataSelectT) {
const [namespaced, setNamespaced] = useState<boolean>(false)
const [namespace, setNamespace] = useState(defaults?.namespace ?? '')
const [filter, setFilter] = useState(defaults?.filter ?? '')
const [enabled, setEnabled] = useState<boolean>(true)

return useMemo(
() =>
context ?? {
enabled,
setEnabled,
namespaced,
setNamespaced,
namespace,
Expand All @@ -53,6 +58,8 @@ export function useDataSelect(defaults?: DataSelectT) {
setFilter,
},
[
enabled,
setEnabled,
context,
namespaced,
setNamespaced,
Expand Down
72 changes: 72 additions & 0 deletions assets/src/generated/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11120,6 +11120,17 @@ export type KubernetesClustersQueryVariables = Exact<{

export type KubernetesClustersQuery = { __typename?: 'RootQueryType', clusters?: { __typename?: 'ClusterConnection', edges?: Array<{ __typename?: 'ClusterEdge', node?: { __typename?: 'Cluster', self?: boolean | null, virtual?: boolean | null, id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, project?: { __typename?: 'Project', id: string, name: string, default?: boolean | null, description?: string | null } | null, pinnedCustomResources?: Array<{ __typename?: 'PinnedCustomResource', id: string, name: string, kind: string, version: string, group: string, displayName: string, namespaced?: boolean | null, cluster?: { __typename?: 'Cluster', self?: boolean | null, virtual?: boolean | null, id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null } | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null> | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null } | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null> | null } | null };

export type KubernetesClusterAuditLogsQueryVariables = Exact<{
clusterId?: InputMaybe<Scalars['ID']['input']>;
first?: InputMaybe<Scalars['Int']['input']>;
after?: InputMaybe<Scalars['String']['input']>;
before?: InputMaybe<Scalars['String']['input']>;
last?: InputMaybe<Scalars['Int']['input']>;
}>;


export type KubernetesClusterAuditLogsQuery = { __typename?: 'RootQueryType', cluster?: { __typename?: 'Cluster', auditLogs?: { __typename?: 'ClusterAuditLogConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, endCursor?: string | null, hasPreviousPage: boolean, startCursor?: string | null }, edges?: Array<{ __typename?: 'ClusterAuditLogEdge', node?: { __typename?: 'ClusterAuditLog', id: string, insertedAt?: string | null, method: string, path: string, actor?: { __typename?: 'User', name: string, email: string } | null } | null } | null> | null } | null } | null };

export type PinCustomResourceMutationVariables = Exact<{
attributes: PinnedCustomResourceAttributes;
}>;
Expand Down Expand Up @@ -22260,6 +22271,66 @@ export type KubernetesClustersQueryHookResult = ReturnType<typeof useKubernetesC
export type KubernetesClustersLazyQueryHookResult = ReturnType<typeof useKubernetesClustersLazyQuery>;
export type KubernetesClustersSuspenseQueryHookResult = ReturnType<typeof useKubernetesClustersSuspenseQuery>;
export type KubernetesClustersQueryResult = Apollo.QueryResult<KubernetesClustersQuery, KubernetesClustersQueryVariables>;
export const KubernetesClusterAuditLogsDocument = gql`
query KubernetesClusterAuditLogs($clusterId: ID, $first: Int, $after: String, $before: String, $last: Int) {
cluster(id: $clusterId) {
auditLogs(first: $first, last: $last, after: $after, before: $before) {
pageInfo {
...PageInfo
}
edges {
node {
id
insertedAt
method
path
actor {
name
email
}
}
}
}
}
}
${PageInfoFragmentDoc}`;

/**
* __useKubernetesClusterAuditLogsQuery__
*
* To run a query within a React component, call `useKubernetesClusterAuditLogsQuery` and pass it any options that fit your needs.
* When your component renders, `useKubernetesClusterAuditLogsQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useKubernetesClusterAuditLogsQuery({
* variables: {
* clusterId: // value for 'clusterId'
* first: // value for 'first'
* after: // value for 'after'
* before: // value for 'before'
* last: // value for 'last'
* },
* });
*/
export function useKubernetesClusterAuditLogsQuery(baseOptions?: Apollo.QueryHookOptions<KubernetesClusterAuditLogsQuery, KubernetesClusterAuditLogsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<KubernetesClusterAuditLogsQuery, KubernetesClusterAuditLogsQueryVariables>(KubernetesClusterAuditLogsDocument, options);
}
export function useKubernetesClusterAuditLogsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<KubernetesClusterAuditLogsQuery, KubernetesClusterAuditLogsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<KubernetesClusterAuditLogsQuery, KubernetesClusterAuditLogsQueryVariables>(KubernetesClusterAuditLogsDocument, options);
}
export function useKubernetesClusterAuditLogsSuspenseQuery(baseOptions?: Apollo.SuspenseQueryHookOptions<KubernetesClusterAuditLogsQuery, KubernetesClusterAuditLogsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useSuspenseQuery<KubernetesClusterAuditLogsQuery, KubernetesClusterAuditLogsQueryVariables>(KubernetesClusterAuditLogsDocument, options);
}
export type KubernetesClusterAuditLogsQueryHookResult = ReturnType<typeof useKubernetesClusterAuditLogsQuery>;
export type KubernetesClusterAuditLogsLazyQueryHookResult = ReturnType<typeof useKubernetesClusterAuditLogsLazyQuery>;
export type KubernetesClusterAuditLogsSuspenseQueryHookResult = ReturnType<typeof useKubernetesClusterAuditLogsSuspenseQuery>;
export type KubernetesClusterAuditLogsQueryResult = Apollo.QueryResult<KubernetesClusterAuditLogsQuery, KubernetesClusterAuditLogsQueryVariables>;
export const PinCustomResourceDocument = gql`
mutation PinCustomResource($attributes: PinnedCustomResourceAttributes!) {
createPinnedCustomResource(attributes: $attributes) {
Expand Down Expand Up @@ -26146,6 +26217,7 @@ export const namedOperations = {
GroupMembers: 'GroupMembers',
UpgradeStatistics: 'UpgradeStatistics',
KubernetesClusters: 'KubernetesClusters',
KubernetesClusterAuditLogs: 'KubernetesClusterAuditLogs',
ArgoRollout: 'ArgoRollout',
Canary: 'Canary',
Certificate: 'Certificate',
Expand Down
28 changes: 28 additions & 0 deletions assets/src/graph/kubernetes.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,34 @@ query KubernetesClusters($projectId: ID) {
}
}

query KubernetesClusterAuditLogs(
$clusterId: ID
$first: Int
$after: String
$before: String
$last: Int
) {
cluster(id: $clusterId) {
auditLogs(first: $first, last: $last, after: $after, before: $before) {
pageInfo {
...PageInfo
}
edges {
node {
id
insertedAt
method
path
actor {
name
email
}
}
}
}
}
}

mutation PinCustomResource($attributes: PinnedCustomResourceAttributes!) {
createPinnedCustomResource(attributes: $attributes) {
...PinnedCustomResource
Expand Down
Loading
Loading