diff --git a/assets/src/components/kubernetes/Navigation.tsx b/assets/src/components/kubernetes/Navigation.tsx index 6685d6b8fb..f7d4636a55 100644 --- a/assets/src/components/kubernetes/Navigation.tsx +++ b/assets/src/components/kubernetes/Navigation.tsx @@ -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' @@ -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() { @@ -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() @@ -125,7 +129,7 @@ export default function Navigation() { }} >
{headerContent}
- + {dataSelect.enabled && } diff --git a/assets/src/components/kubernetes/audit/Audit.tsx b/assets/src/components/kubernetes/audit/Audit.tsx new file mode 100644 index 0000000000..92869e7b7e --- /dev/null +++ b/assets/src/components/kubernetes/audit/Audit.tsx @@ -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() + +const RawColumn = ({ getValue }: CellContext): 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 }) => , +}) + +const userColumn = columnHelper.accessor((log) => log?.actor, { + id: 'user', + header: 'User', + enableSorting: true, + cell: ({ 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 ( + + {error && } + + + ) +} diff --git a/assets/src/components/kubernetes/common/DataSelect.tsx b/assets/src/components/kubernetes/common/DataSelect.tsx index 5dd327737d..6c45d743bd 100644 --- a/assets/src/components/kubernetes/common/DataSelect.tsx +++ b/assets/src/components/kubernetes/common/DataSelect.tsx @@ -24,6 +24,8 @@ export type DataSelectT = { } export type DataSelectContextT = { + enabled: boolean + setEnabled: Dispatch> namespaced: boolean setNamespaced: Dispatch> namespace: string @@ -41,10 +43,13 @@ export function useDataSelect(defaults?: DataSelectT) { const [namespaced, setNamespaced] = useState(false) const [namespace, setNamespace] = useState(defaults?.namespace ?? '') const [filter, setFilter] = useState(defaults?.filter ?? '') + const [enabled, setEnabled] = useState(true) return useMemo( () => context ?? { + enabled, + setEnabled, namespaced, setNamespaced, namespace, @@ -53,6 +58,8 @@ export function useDataSelect(defaults?: DataSelectT) { setFilter, }, [ + enabled, + setEnabled, context, namespaced, setNamespaced, diff --git a/assets/src/generated/graphql.ts b/assets/src/generated/graphql.ts index 6ce13a4558..02b27ee5e5 100644 --- a/assets/src/generated/graphql.ts +++ b/assets/src/generated/graphql.ts @@ -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; + first?: InputMaybe; + after?: InputMaybe; + before?: InputMaybe; + last?: InputMaybe; +}>; + + +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; }>; @@ -22260,6 +22271,66 @@ export type KubernetesClustersQueryHookResult = ReturnType; export type KubernetesClustersSuspenseQueryHookResult = ReturnType; export type KubernetesClustersQueryResult = Apollo.QueryResult; +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) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(KubernetesClusterAuditLogsDocument, options); + } +export function useKubernetesClusterAuditLogsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(KubernetesClusterAuditLogsDocument, options); + } +export function useKubernetesClusterAuditLogsSuspenseQuery(baseOptions?: Apollo.SuspenseQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useSuspenseQuery(KubernetesClusterAuditLogsDocument, options); + } +export type KubernetesClusterAuditLogsQueryHookResult = ReturnType; +export type KubernetesClusterAuditLogsLazyQueryHookResult = ReturnType; +export type KubernetesClusterAuditLogsSuspenseQueryHookResult = ReturnType; +export type KubernetesClusterAuditLogsQueryResult = Apollo.QueryResult; export const PinCustomResourceDocument = gql` mutation PinCustomResource($attributes: PinnedCustomResourceAttributes!) { createPinnedCustomResource(attributes: $attributes) { @@ -26146,6 +26217,7 @@ export const namedOperations = { GroupMembers: 'GroupMembers', UpgradeStatistics: 'UpgradeStatistics', KubernetesClusters: 'KubernetesClusters', + KubernetesClusterAuditLogs: 'KubernetesClusterAuditLogs', ArgoRollout: 'ArgoRollout', Canary: 'Canary', Certificate: 'Certificate', diff --git a/assets/src/graph/kubernetes.graphql b/assets/src/graph/kubernetes.graphql index 92faeccd8c..a5c8569f9a 100644 --- a/assets/src/graph/kubernetes.graphql +++ b/assets/src/graph/kubernetes.graphql @@ -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 diff --git a/assets/src/routes/kubernetesRoute.tsx b/assets/src/routes/kubernetesRoute.tsx index c7423657c4..92880f5677 100644 --- a/assets/src/routes/kubernetesRoute.tsx +++ b/assets/src/routes/kubernetesRoute.tsx @@ -1,4 +1,8 @@ -import { Navigate, Route } from 'react-router-dom' +import Secret, { SecretData } from 'components/kubernetes/configuration/Secret' +import CustomResourceDefinition, { + CustomResourceDefinitionConditions, + CustomResourceDefinitionObjects, +} from 'components/kubernetes/customresources/CustomResourceDefinition' import Service, { ServiceEvents, @@ -6,54 +10,29 @@ import Service, { ServiceIngresses, ServicePods, } from 'components/kubernetes/network/Service' -import PersistentVolume, { - PersistentVolumeInfo, -} from 'components/kubernetes/storage/PersistentVolume' -import Secret, { SecretData } from 'components/kubernetes/configuration/Secret' -import RoleBinding, { - RoleBindingSubjects, -} from 'components/kubernetes/rbac/RoleBinding' -import Role, { RolePolicyRules } from 'components/kubernetes/rbac/Role' import ClusterRole from 'components/kubernetes/rbac/ClusterRole' import ClusterRoleBinding, { ClusterRoleBindingSubjects, } from 'components/kubernetes/rbac/ClusterRoleBinding' -import CustomResourceDefinition, { - CustomResourceDefinitionObjects, - CustomResourceDefinitionConditions, -} from 'components/kubernetes/customresources/CustomResourceDefinition' +import Role, { RolePolicyRules } from 'components/kubernetes/rbac/Role' +import RoleBinding, { + RoleBindingSubjects, +} from 'components/kubernetes/rbac/RoleBinding' +import PersistentVolume, { + PersistentVolumeInfo, +} from 'components/kubernetes/storage/PersistentVolume' +import { Navigate, Route } from 'react-router-dom' +import Audit from '../components/kubernetes/audit/Audit.tsx' -import { - Pod, - PodContainers, - PodEvents, - PodExec, - PodInfo, - PodLogs, -} from '../components/kubernetes/workloads/Pod' -import Navigation from '../components/kubernetes/Navigation' -import Workloads from '../components/kubernetes/workloads/Workloads' -import Network from '../components/kubernetes/network/Network' -import Services from '../components/kubernetes/network/Services' -import Ingresses from '../components/kubernetes/network/Ingresses' -import IngressClasses from '../components/kubernetes/network/IngressClasses' -import Storage from '../components/kubernetes/storage/Storage' -import Configuration from '../components/kubernetes/configuration/Configuration' -import Deployments from '../components/kubernetes/workloads/Deployments' -import Pods from '../components/kubernetes/workloads/Pods' -import ReplicaSets from '../components/kubernetes/workloads/ReplicaSets' -import StatefulSets from '../components/kubernetes/workloads/StatefulSets' -import DaemonSets from '../components/kubernetes/workloads/DaemonSets' -import Jobs from '../components/kubernetes/workloads/Jobs' -import CronJobs from '../components/kubernetes/workloads/CronJobs' -import ReplicationControllers from '../components/kubernetes/workloads/ReplicationControllers' -import PersistentVolumeClaims from '../components/kubernetes/storage/PersistentVolumeClaims' -import PersistentVolumes from '../components/kubernetes/storage/PersistentVolumes' -import StorageClasses from '../components/kubernetes/storage/StorageClasses' -import ConfigMaps from '../components/kubernetes/configuration/ConfigMaps' -import Secrets from '../components/kubernetes/configuration/Secrets' +import Root from '../components/kubernetes/Cluster' import Cluster from '../components/kubernetes/cluster/Cluster' -import Nodes from '../components/kubernetes/cluster/Nodes' +import Events from '../components/kubernetes/cluster/Events' + +import HorizontalPodAutoscalers from '../components/kubernetes/cluster/HorizontalPodAutoscalers' +import Namespace, { + NamespaceEvents, +} from '../components/kubernetes/cluster/Namespace' +import Namespaces from '../components/kubernetes/cluster/Namespaces' import Node, { NodeConditions, NodeContainerImages, @@ -61,96 +40,119 @@ import Node, { NodeInfo, NodePods, } from '../components/kubernetes/cluster/Node' -import Events from '../components/kubernetes/cluster/Events' -import Namespaces from '../components/kubernetes/cluster/Namespaces' +import Nodes from '../components/kubernetes/cluster/Nodes' + +import Raw from '../components/kubernetes/common/Raw' +import ConfigMap, { + ConfigMapData, +} from '../components/kubernetes/configuration/ConfigMap' +import ConfigMaps from '../components/kubernetes/configuration/ConfigMaps' +import Configuration from '../components/kubernetes/configuration/Configuration' +import Secrets from '../components/kubernetes/configuration/Secrets' + +import CustomResource, { + CustomResourceEvents, +} from '../components/kubernetes/customresources/CustomResource' import CustomResourceDefinitions from '../components/kubernetes/customresources/CustomResourceDefinitions' +import Navigation from '../components/kubernetes/Navigation' +import Ingress, { + IngressEvents, + IngressInfo, +} from '../components/kubernetes/network/Ingress' +import IngressClass from '../components/kubernetes/network/IngressClass' +import IngressClasses from '../components/kubernetes/network/IngressClasses' +import Ingresses from '../components/kubernetes/network/Ingresses' +import Network from '../components/kubernetes/network/Network' import NetworkPolicies from '../components/kubernetes/network/NetworkPolicies' +import NetworkPolicy, { + NetworkPolicyInfo, +} from '../components/kubernetes/network/NetworkPolicy' +import Services from '../components/kubernetes/network/Services' import ClusterRoleBindings from '../components/kubernetes/rbac/ClusterRoleBindings' import ClusterRoles from '../components/kubernetes/rbac/ClusterRoles' +import Rbac from '../components/kubernetes/rbac/Rbac' import RoleBindings from '../components/kubernetes/rbac/RoleBindings' import Roles from '../components/kubernetes/rbac/Roles' -import Rbac from '../components/kubernetes/rbac/Rbac' + +import ServiceAccount from '../components/kubernetes/rbac/ServiceAccount' + +import ServiceAccounts from '../components/kubernetes/rbac/ServiceAccounts' +import PersistentVolumeClaim from '../components/kubernetes/storage/PersistentVolumeClaim' +import PersistentVolumeClaims from '../components/kubernetes/storage/PersistentVolumeClaims' +import PersistentVolumes from '../components/kubernetes/storage/PersistentVolumes' +import Storage from '../components/kubernetes/storage/Storage' +import StorageClass, { + StorageClassPersistentVolumes, +} from '../components/kubernetes/storage/StorageClass' +import StorageClasses from '../components/kubernetes/storage/StorageClasses' +import CronJob, { + CronJobEvents, + CronJobJobs, +} from '../components/kubernetes/workloads/CronJob' +import CronJobs from '../components/kubernetes/workloads/CronJobs' +import DaemonSet, { + DaemonSetEvents, + DaemonSetPods, + DaemonSetServices, +} from '../components/kubernetes/workloads/DaemonSet' +import DaemonSets from '../components/kubernetes/workloads/DaemonSets' import Deployment, { DeploymentEvents, DeploymentHorizontalPodAutoscalers, DeploymentReplicaSets, } from '../components/kubernetes/workloads/Deployment' +import Deployments from '../components/kubernetes/workloads/Deployments' +import Job, { + JobConditions, + JobEvents, + JobPods, +} from '../components/kubernetes/workloads/Job' +import Jobs from '../components/kubernetes/workloads/Jobs' + +import { + Pod, + PodContainers, + PodEvents, + PodExec, + PodInfo, + PodLogs, +} from '../components/kubernetes/workloads/Pod' +import Pods from '../components/kubernetes/workloads/Pods' import ReplicaSet, { ReplicaSetEvents, ReplicaSetInfo, ReplicaSetPods, ReplicaSetServices, } from '../components/kubernetes/workloads/ReplicaSet' -import StatefulSet, { - StatefulSetEvents, - StatefulSetPods, -} from '../components/kubernetes/workloads/StatefulSet' -import DaemonSet, { - DaemonSetEvents, - DaemonSetPods, - DaemonSetServices, -} from '../components/kubernetes/workloads/DaemonSet' -import Job, { - JobConditions, - JobEvents, - JobPods, -} from '../components/kubernetes/workloads/Job' +import ReplicaSets from '../components/kubernetes/workloads/ReplicaSets' import ReplicationController, { ReplicationControllerEvents, ReplicationControllerPods, ReplicationControllerServices, } from '../components/kubernetes/workloads/ReplicationController' -import Ingress, { - IngressEvents, - IngressInfo, -} from '../components/kubernetes/network/Ingress' -import CronJob, { - CronJobEvents, - CronJobJobs, -} from '../components/kubernetes/workloads/CronJob' -import IngressClass from '../components/kubernetes/network/IngressClass' -import NetworkPolicy, { - NetworkPolicyInfo, -} from '../components/kubernetes/network/NetworkPolicy' -import PersistentVolumeClaim from '../components/kubernetes/storage/PersistentVolumeClaim' -import StorageClass, { - StorageClassPersistentVolumes, -} from '../components/kubernetes/storage/StorageClass' -import ConfigMap, { - ConfigMapData, -} from '../components/kubernetes/configuration/ConfigMap' -import Namespace, { - NamespaceEvents, -} from '../components/kubernetes/cluster/Namespace' - -import Raw from '../components/kubernetes/common/Raw' - -import ServiceAccounts from '../components/kubernetes/rbac/ServiceAccounts' - -import ServiceAccount from '../components/kubernetes/rbac/ServiceAccount' - -import HorizontalPodAutoscalers from '../components/kubernetes/cluster/HorizontalPodAutoscalers' - -import CustomResource, { - CustomResourceEvents, -} from '../components/kubernetes/customresources/CustomResource' - -import Root from '../components/kubernetes/Cluster' +import ReplicationControllers from '../components/kubernetes/workloads/ReplicationControllers' +import StatefulSet, { + StatefulSetEvents, + StatefulSetPods, +} from '../components/kubernetes/workloads/StatefulSet' +import StatefulSets from '../components/kubernetes/workloads/StatefulSets' +import Workloads from '../components/kubernetes/workloads/Workloads' import { + AUDIT_REL_PATH, CLUSTER_REL_PATH, - CLUSTER_ROLES_REL_PATH, CLUSTER_ROLE_BINDINGS_REL_PATH, - CONFIGURATION_REL_PATH, + CLUSTER_ROLES_REL_PATH, CONFIG_MAPS_REL_PATH, + CONFIGURATION_REL_PATH, CRON_JOBS_REL_PATH, CUSTOM_RESOURCES_REL_PATH, DAEMON_SETS_REL_PATH, DEPLOYMENTS_REL_PATH, EVENTS_REL_PATH, HPAS_REL_PATH, - INGRESSES_REL_PATH, INGRESS_CLASSES_REL_PATH, + INGRESSES_REL_PATH, JOBS_REL_PATH, KUBERNETES_ABS_PATH, NAMESPACED_RESOURCE_DETAILS_REL_PATH, @@ -158,18 +160,18 @@ import { NETWORK_POLICIES_REL_PATH, NETWORK_REL_PATH, NODES_REL_PATH, - PERSISTENT_VOLUMES_REL_PATH, PERSISTENT_VOLUME_CLAIMS_REL_PATH, + PERSISTENT_VOLUMES_REL_PATH, PODS_REL_PATH, RBAC_REL_PATH, - REPLICATION_CONTROLLERS_REL_PATH, REPLICA_SETS_REL_PATH, + REPLICATION_CONTROLLERS_REL_PATH, RESOURCE_DETAILS_REL_PATH, - ROLES_REL_PATH, ROLE_BINDINGS_REL_PATH, + ROLES_REL_PATH, SECRETS_REL_PATH, - SERVICES_REL_PATH, SERVICE_ACCOUNTS_REL_PATH, + SERVICES_REL_PATH, STATEFUL_SETS_REL_PATH, STORAGE_CLASSES_REL_PATH, STORAGE_REL_PATH, @@ -386,6 +388,10 @@ export const kubernetesRoutes = ( path={CUSTOM_RESOURCES_REL_PATH} element={} /> + } + />