diff --git a/frontend/plugins/kubepanel/public/create-resource-templates/Ingress.yaml b/frontend/plugins/kubepanel/public/create-resource-templates/Ingress.yaml new file mode 100644 index 00000000000..76640b9447f --- /dev/null +++ b/frontend/plugins/kubepanel/public/create-resource-templates/Ingress.yaml @@ -0,0 +1,17 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: minimal-ingress + annotations: + nginx.ingress.kubernetes.io/rewrite-target: / +spec: + rules: + - http: + paths: + - path: /testpath + pathType: Prefix + backend: + service: + name: test + port: + number: 80 diff --git a/frontend/plugins/kubepanel/public/create-resource-templates/Secret.yaml b/frontend/plugins/kubepanel/public/create-resource-templates/Secret.yaml new file mode 100644 index 00000000000..191e2d7794f --- /dev/null +++ b/frontend/plugins/kubepanel/public/create-resource-templates/Secret.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Secret +metadata: + name: secret-basic-auth +type: kubernetes.io/basic-auth +stringData: + username: admin + password: t0p-Secret diff --git a/frontend/plugins/kubepanel/src/components/chart/pie-chart.tsx b/frontend/plugins/kubepanel/src/components/chart/pie-chart.tsx index d5f9eb6dcbf..db279efaca1 100644 --- a/frontend/plugins/kubepanel/src/components/chart/pie-chart.tsx +++ b/frontend/plugins/kubepanel/src/components/chart/pie-chart.tsx @@ -35,7 +35,7 @@ export const PieChart = ({ title, data, color }: PieChartProps) => {
{title}
{sum(data.map((d) => d.value))}
- ); + ) as unknown as string; } } }, diff --git a/frontend/plugins/kubepanel/src/components/common/section/section.tsx b/frontend/plugins/kubepanel/src/components/common/section/section.tsx new file mode 100644 index 00000000000..89965ab7a1b --- /dev/null +++ b/frontend/plugins/kubepanel/src/components/common/section/section.tsx @@ -0,0 +1,17 @@ +interface SectionProps { + children: React.ReactNode; +} + +/** + * Renders a section component with the provided children. + * + * @param {SectionProps} children - The children to render within the section. + * @return {JSX.Element} The rendered section component. + */ +export function Section({ children }: SectionProps) { + return ( +
+ {children} +
+ ); +} diff --git a/frontend/plugins/kubepanel/src/components/common/title/title.tsx b/frontend/plugins/kubepanel/src/components/common/title/title.tsx new file mode 100644 index 00000000000..0f339a650b1 --- /dev/null +++ b/frontend/plugins/kubepanel/src/components/common/title/title.tsx @@ -0,0 +1,22 @@ +interface PageTitleProps { + children: string; + type: 'primary' | 'secondary' | 'table'; +} + +const titleClassName: Record = { + primary: 'text-2xl font-medium', + secondary: 'text-xl font-medium', + table: 'text-xs font-medium text-gray-500' +}; + +/** + * Renders a title component with the specified type and children. + * + * @param {PageTitleProps} props - The properties of the title component. + * @param {ReactNode} props.children - The content of the title component. + * @param {string} props.type - The type of the title component. + * @return {JSX.Element} The rendered title component. + */ +export default function Title({ children, type }: PageTitleProps) { + return {children}; +} diff --git a/frontend/plugins/kubepanel/src/components/kube/kube-badge.tsx b/frontend/plugins/kubepanel/src/components/kube/kube-badge.tsx index 80f676c98de..3b361cc79ec 100644 --- a/frontend/plugins/kubepanel/src/components/kube/kube-badge.tsx +++ b/frontend/plugins/kubepanel/src/components/kube/kube-badge.tsx @@ -1,6 +1,6 @@ import { useEffect, useRef, useState } from 'react'; -export interface KubeBadgeProps { +interface KubeBadgeProps { label: React.ReactNode; disabled?: boolean; expandable?: boolean; @@ -32,14 +32,14 @@ export const KubeBadge = ({ }; const { textColor, backgroundColor } = color ?? {}; - const disabledClass = disabled ? 'opacity-50 cursor-not-allowed' : 'opacity-100 cursor-pointer'; + const disabledClass = disabled && 'opacity-50 cursor-not-allowed'; + const expandedClass = isExpanded ? 'break-words' : 'truncate'; + const expandableClass = isExpandable && 'cursor-pointer'; + const bgColorClass = backgroundColor ? `bg-${backgroundColor}` : 'bg-[#EFF0F1]'; return (
diff --git a/frontend/plugins/kubepanel/src/components/kube/object/detail/kube-object-detail-info-list.tsx b/frontend/plugins/kubepanel/src/components/kube/object/detail/kube-object-detail-info-list.tsx index 339cd5ae100..673057898cc 100644 --- a/frontend/plugins/kubepanel/src/components/kube/object/detail/kube-object-detail-info-list.tsx +++ b/frontend/plugins/kubepanel/src/components/kube/object/detail/kube-object-detail-info-list.tsx @@ -1,4 +1,4 @@ -import { DrawerItem } from '../../../../pages/kubepanel/components/drawer/drawer-item'; +import DrawerItem from '../../../../pages/kubepanel/components/drawer/drawer-item'; import { KubeObjectAge } from '../kube-object-age'; import { LocaleDate } from '../../local-date'; import moment from 'moment-timezone'; diff --git a/frontend/plugins/kubepanel/src/constants/kube-api.ts b/frontend/plugins/kubepanel/src/constants/kube-api.ts index acc46f4114f..ffad20f76f2 100644 --- a/frontend/plugins/kubepanel/src/constants/kube-api.ts +++ b/frontend/plugins/kubepanel/src/constants/kube-api.ts @@ -33,5 +33,23 @@ export const ApiBaseParamsMap: Record = { apiGroup: undefined, apiVersion: 'v1', resource: Resources.PersistentVolumeClaims + }, + secrets: { + apiPrefix: 'api', + apiGroup: undefined, + apiVersion: 'v1', + resource: Resources.Secrets + }, + ingresses: { + apiPrefix: 'apis', + apiGroup: 'networking.k8s.io', + apiVersion: 'v1', + resource: Resources.Ingresses + }, + events: { + apiPrefix: 'api', + apiGroup: undefined, + apiVersion: 'v1', + resource: Resources.Events } }; diff --git a/frontend/plugins/kubepanel/src/constants/kube-object.ts b/frontend/plugins/kubepanel/src/constants/kube-object.ts index 7566549dbee..10eda5001d6 100644 --- a/frontend/plugins/kubepanel/src/constants/kube-object.ts +++ b/frontend/plugins/kubepanel/src/constants/kube-object.ts @@ -1,8 +1,11 @@ import { ConfigMap, Deployment, + Ingress, + KubeEvent, PersistentVolumeClaim, Pod, + Secret, StatefulSet } from '@/k8slens/kube-object'; import { StringKeyOf } from 'type-fest'; @@ -14,7 +17,10 @@ export enum Resources { Deployments = 'deployments', StatefulSets = 'statefulsets', ConfigMaps = 'configmaps', - PersistentVolumeClaims = 'persistentvolumeclaims' + PersistentVolumeClaims = 'persistentvolumeclaims', + Secrets = 'secrets', + Ingresses = 'ingresses', + Events = 'events' } export const KubeObjectConstructorMap: { [key in Resources]: any } = { @@ -22,7 +28,10 @@ export const KubeObjectConstructorMap: { [key in Resources]: any } = { [Resources.Deployments]: Deployment, [Resources.StatefulSets]: StatefulSet, [Resources.ConfigMaps]: ConfigMap, - [Resources.PersistentVolumeClaims]: PersistentVolumeClaim + [Resources.PersistentVolumeClaims]: PersistentVolumeClaim, + [Resources.Secrets]: Secret, + [Resources.Ingresses]: Ingress, + [Resources.Events]: KubeEvent }; export const KindMap: { [key in Resources]: any } = { @@ -30,5 +39,8 @@ export const KindMap: { [key in Resources]: any } = { [Resources.Deployments]: 'Deployment', [Resources.StatefulSets]: 'StatefulSet', [Resources.ConfigMaps]: 'ConfigMap', - [Resources.PersistentVolumeClaims]: 'PersistentVolumeClaim' + [Resources.PersistentVolumeClaims]: 'PersistentVolumeClaim', + [Resources.Secrets]: 'Secret', + [Resources.Ingresses]: 'Ingress', + [Resources.Events]: 'Event' }; diff --git a/frontend/plugins/kubepanel/src/constants/theme.ts b/frontend/plugins/kubepanel/src/constants/theme.ts index 9750a5235bf..07a685f2759 100644 --- a/frontend/plugins/kubepanel/src/constants/theme.ts +++ b/frontend/plugins/kubepanel/src/constants/theme.ts @@ -8,6 +8,10 @@ export const theme: ThemeConfig = { }, Collapse: { headerPadding: 0 + }, + Table: { + headerBorderRadius: 0, + headerBg: '#F6F8F9' } } }; diff --git a/frontend/plugins/kubepanel/src/pages/kubepanel/components/action-button/update-editor-modal.tsx b/frontend/plugins/kubepanel/src/pages/kubepanel/components/action-button/update-editor-modal.tsx index adc7843f752..fca6cf9f562 100644 --- a/frontend/plugins/kubepanel/src/pages/kubepanel/components/action-button/update-editor-modal.tsx +++ b/frontend/plugins/kubepanel/src/pages/kubepanel/components/action-button/update-editor-modal.tsx @@ -7,7 +7,7 @@ import { editor } from 'monaco-editor'; import { useEffect, useRef, useState } from 'react'; interface Props { - obj: K; + obj?: K; open: boolean; onUpdate: (data: string) => Promise; onCancel: () => void; @@ -21,6 +21,8 @@ const UpdateEditorModal = ({ onCancel, onOk }: Props) => { + if (!obj) return null; + const [clickedUpdate, setClickedUpdate] = useState(false); const [msgApi, contextHolder] = message.useMessage(); const editorRef = useRef(); diff --git a/frontend/plugins/kubepanel/src/pages/kubepanel/components/drawer/drawer-collapse.tsx b/frontend/plugins/kubepanel/src/pages/kubepanel/components/drawer/drawer-collapse.tsx index 81208a789a6..cd3f27e2f93 100644 --- a/frontend/plugins/kubepanel/src/pages/kubepanel/components/drawer/drawer-collapse.tsx +++ b/frontend/plugins/kubepanel/src/pages/kubepanel/components/drawer/drawer-collapse.tsx @@ -1,4 +1,4 @@ -import { DrawerItem } from '@/pages/kubepanel/components/drawer/drawer-item'; +import DrawerItem from '@/pages/kubepanel/components/drawer/drawer-item'; import { Collapse } from 'antd'; interface Props { diff --git a/frontend/plugins/kubepanel/src/pages/kubepanel/components/drawer/drawer-item.tsx b/frontend/plugins/kubepanel/src/pages/kubepanel/components/drawer/drawer-item.tsx index d9bfc82dbc3..4d9b7f7d6a3 100644 --- a/frontend/plugins/kubepanel/src/pages/kubepanel/components/drawer/drawer-item.tsx +++ b/frontend/plugins/kubepanel/src/pages/kubepanel/components/drawer/drawer-item.tsx @@ -5,11 +5,7 @@ interface DrawerItemProps { padding?: boolean; } -export const DrawerItem = ({ - name, - value, - hidden = false, -}: DrawerItemProps) => { +const DrawerItem = ({ name, value, hidden = false }: DrawerItemProps) => { if (hidden) return null; return ( @@ -17,12 +13,10 @@ export const DrawerItem = ({ className={`grid last:border-b-0`} style={{ gridTemplateColumns: 'minmax(30%, min-content) auto' }} > - - {name} - - - {value} - + {name} + {value}
); }; + +export default DrawerItem; \ No newline at end of file diff --git a/frontend/plugins/kubepanel/src/pages/kubepanel/components/drawer/drawer.tsx b/frontend/plugins/kubepanel/src/pages/kubepanel/components/drawer/drawer.tsx index 2ada0eb88ee..5f207b6fe20 100644 --- a/frontend/plugins/kubepanel/src/pages/kubepanel/components/drawer/drawer.tsx +++ b/frontend/plugins/kubepanel/src/pages/kubepanel/components/drawer/drawer.tsx @@ -9,10 +9,15 @@ interface Props { const Drawer = ({ title, children, open, onClose }: Props) => { return ( } - title={{title}} + closeIcon={} + title={{title}} width="550px" onClose={onClose} destroyOnClose diff --git a/frontend/plugins/kubepanel/src/pages/kubepanel/components/kube-object/config/secret/secret-detail.tsx b/frontend/plugins/kubepanel/src/pages/kubepanel/components/kube-object/config/secret/secret-detail.tsx new file mode 100644 index 00000000000..f912294a98f --- /dev/null +++ b/frontend/plugins/kubepanel/src/pages/kubepanel/components/kube-object/config/secret/secret-detail.tsx @@ -0,0 +1,117 @@ +import { Secret } from '@/k8slens/kube-object'; +import DrawerPanel from '../../../drawer/drawer-panel'; +import { KubeObjectInfoList } from '@/components/kube/object/detail/kube-object-detail-info-list'; +import Drawer from '../../../drawer/drawer'; +import React, { Suspense } from 'react'; +import { Button, Input, Space } from 'antd'; +import { EyeInvisibleOutlined, EyeOutlined } from '@ant-design/icons'; +import { DetailDrawerProps } from '@/types/detail'; +import { entries } from 'lodash'; +import { updateResource } from '@/api/update'; +import { Resources } from '@/constants/kube-object'; +import { dumpKubeObject } from '@/utils/yaml'; +import useMessage from 'antd/lib/message/useMessage'; + +type RevealableInput = { + name: string; + initValue: string; + onChange?: (value: string) => void; +}; + +const SecretDataInput = ({ name, initValue, onChange }: RevealableInput) => { + const [revealed, setRevealed] = React.useState(false); + const [value, setValue] = React.useState(initValue); + return ( +
+
{name}
+ + { + setValue(e.target.value); + onChange?.(e.target.value); + }} + value={revealed ? value : ''} + /> +
+ ); +}; + +const SecretDataForm = ({ secret }: { secret: Secret }) => { + const [isSaving, setIsSaving] = React.useState(false); + const [msgApi, contextHolder] = useMessage({}); + const dataMap = React.useRef>>(secret.data).current; + + return ( + <> + {contextHolder} + {entries(secret.data).map(([name, value]) => ( + { + dataMap[name] = Buffer.from(value, 'ascii').toString('base64'); + }} + /> + ))} + + + + + ); +}; + +const SecretDetail = ({ obj, open, onClose }: DetailDrawerProps) => { + if (!obj || !(obj instanceof Secret)) return null; + + return ( + + + + + + + + + ); +}; + +export default SecretDetail; diff --git a/frontend/plugins/kubepanel/src/pages/kubepanel/components/kube-object/config/secret/secret.tsx b/frontend/plugins/kubepanel/src/pages/kubepanel/components/kube-object/config/secret/secret.tsx new file mode 100644 index 00000000000..968b099a787 --- /dev/null +++ b/frontend/plugins/kubepanel/src/pages/kubepanel/components/kube-object/config/secret/secret.tsx @@ -0,0 +1,81 @@ +import { KubeBadge } from '@/components/kube/kube-badge'; +import { KubeObjectAge } from '@/components/kube/object/kube-object-age'; +import { Secret } from '@/k8slens/kube-object'; +import { ColumnsType } from 'antd/lib/table'; +import ActionButton from '../../../action-button/action-button'; +import { deleteResource } from '@/api/delete'; +import { updateResource } from '@/api/update'; +import { Resources } from '@/constants/kube-object'; +import Table from '../../../table/table'; +import { useQuery } from '@tanstack/react-query'; +import { useState } from 'react'; +import { fetchData, useSecretStore } from '@/store/kube'; +import SecretDetail from './secret-detail'; + +const columns: ColumnsType = [ + { + title: 'Name', + key: 'name', + fixed: 'left', + render: (_, secret) => secret.getName() + }, + { + title: 'Labels', + key: 'labels', + render: (_, secret) => + secret.getLabels().map((label) => ) + }, + { + title: 'Keys', + key: 'keys', + render: (_, secret) => secret.getKeys().join(', ') + }, + { + title: 'Type', + key: 'type', + dataIndex: 'type' + }, + { + title: 'Age', + key: 'age', + render: (_, secret) => + }, + { + key: 'action', + fixed: 'right', + render: (_, secret) => ( + updateResource(data, secret.getName(), Resources.Secrets)} + onDelete={() => deleteResource(secret.getName(), Resources.Secrets)} + /> + ) + } +]; + +const SecretOverviewPage = () => { + const [secret, setSecret] = useState(); + const [openDrawer, setOpenDrawer] = useState(false); + const { items, replace } = useSecretStore(); + + useQuery(['secrets'], () => fetchData(replace, Resources.Secrets), { refetchInterval: 5000 }); + + return ( + <> + ({ + onClick: () => { + setSecret(secret); + setOpenDrawer(true); + } + })} + /> + setOpenDrawer(false)} /> + + ); +}; + +export default SecretOverviewPage; diff --git a/frontend/plugins/kubepanel/src/pages/kubepanel/components/kube-object/network/ingress/ingress-detail.tsx b/frontend/plugins/kubepanel/src/pages/kubepanel/components/kube-object/network/ingress/ingress-detail.tsx new file mode 100644 index 00000000000..1c00a97e797 --- /dev/null +++ b/frontend/plugins/kubepanel/src/pages/kubepanel/components/kube-object/network/ingress/ingress-detail.tsx @@ -0,0 +1,112 @@ +import { KubeObjectInfoList } from '@/components/kube/object/detail/kube-object-detail-info-list'; +import { + ComputedIngressRoute, + ILoadBalancerIngress, + Ingress, + computeRuleDeclarations +} from '@/k8slens/kube-object'; +import { DetailDrawerProps } from '@/types/detail'; +import DrawerPanel from '../../../drawer/drawer-panel'; +import Drawer from '../../../drawer/drawer'; +import DrawerItem from '../../../drawer/drawer-item'; +import { Button, Table } from 'antd'; +import { ColumnsType } from 'antd/lib/table'; + +const rulesColumns: ColumnsType = [ + { + key: 'path', + title: 'Path', + dataIndex: 'pathname', + render: (pathname: string) => (pathname === '' ? '_' : pathname) + }, + { + key: 'link', + title: 'Link', + render: (_, { displayAsLink, url }) => + displayAsLink ? ( + + ) : ( + url + ) + }, + { + key: 'backends', + title: 'Backends', + dataIndex: 'service' + } +]; + +const pointsColumns: ColumnsType = [ + { + key: 'hostname', + title: 'Hostname', + dataIndex: 'hostname', + render: (hostname?: string) => (hostname ? '_' : hostname) + }, + { + key: 'ip', + title: 'IP', + dataIndex: 'ip', + render: (ip?: string) => (ip ? '_' : ip) + } +]; + +const IngressPoints = ({ points }: { points?: ILoadBalancerIngress[] }) => { + if (!points || points.length === 0) { + return null; + } + + return ( +
+ ); +}; + +const IngressDetail = ({ obj, open, onClose }: DetailDrawerProps) => { + if (!obj || !(obj instanceof Ingress)) return null; + + const port = obj.getServiceNamePort(); + return ( + + + + + ( +

{tls.secretName}

+ ))} + /> + {port && } +
+ + {obj.getRules().map((rule, idx) => ( +
+ {rule.http && ( +
<>{rule.host && `Host: ${rule.host}`}} + /> + )} + + ))} + + + + + + ); +}; + +export default IngressDetail; diff --git a/frontend/plugins/kubepanel/src/pages/kubepanel/components/kube-object/network/ingress/ingress.tsx b/frontend/plugins/kubepanel/src/pages/kubepanel/components/kube-object/network/ingress/ingress.tsx new file mode 100644 index 00000000000..4ea17311f3c --- /dev/null +++ b/frontend/plugins/kubepanel/src/pages/kubepanel/components/kube-object/network/ingress/ingress.tsx @@ -0,0 +1,96 @@ +import { KubeObjectAge } from '@/components/kube/object/kube-object-age'; +import { Ingress, computeRouteDeclarations } from '@/k8slens/kube-object'; +import { ColumnsType } from 'antd/lib/table'; +import ActionButton from '../../../action-button/action-button'; +import { deleteResource } from '@/api/delete'; +import { updateResource } from '@/api/update'; +import { Resources } from '@/constants/kube-object'; +import Table from '../../../table/table'; +import { useQuery } from '@tanstack/react-query'; +import { useState } from 'react'; +import { fetchData, useIngressStore } from '@/store/kube'; +import { Button } from 'antd'; +import IngressDetail from './ingress-detail'; + +const columns: ColumnsType = [ + { + title: 'Name', + key: 'name', + fixed: 'left', + render: (_, ingress) => ingress.getName() + }, + { + title: 'LoadBalancers', + key: 'load-balancers', + render: (_, ingress) => ingress.getLoadBalancers().map((lb) =>

{lb}

) + }, + { + title: 'Age', + key: 'age', + render: (_, ingress) => + }, + { + title: 'Rules', + key: 'rules', + render: (_, ingress) => + computeRouteDeclarations(ingress).map((decl) => ( +
+ {decl.displayAsLink ? ( + <> + + {` ⇢ ${decl.service}`} + + ) : ( + <>{`${decl.url} ⇢ ${decl.service}`} + )} +
+ )) + }, + { + key: 'action', + fixed: 'right', + render: (_, ingress) => ( + updateResource(data, ingress.getName(), Resources.Ingresses)} + onDelete={() => deleteResource(ingress.getName(), Resources.Ingresses)} + /> + ) + } +]; + +const IngressOverviewPage = () => { + const [ingress, setIngress] = useState(); + const [openDrawer, setOpenDrawer] = useState(false); + const { items, replace } = useIngressStore(); + + useQuery(['ingresss'], () => fetchData(replace, Resources.Ingresses), { refetchInterval: 5000 }); + + return ( + <> +
({ + onClick: () => { + setIngress(ingress); + setOpenDrawer(true); + } + })} + /> + setOpenDrawer(false)} /> + + ); +}; + +export default IngressOverviewPage; diff --git a/frontend/plugins/kubepanel/src/pages/kubepanel/components/kube-object/storage/volume-claim/volume-claim-detail.tsx b/frontend/plugins/kubepanel/src/pages/kubepanel/components/kube-object/storage/volume-claim/volume-claim-detail.tsx index 29e2dd051a1..c23deea5fd8 100644 --- a/frontend/plugins/kubepanel/src/pages/kubepanel/components/kube-object/storage/volume-claim/volume-claim-detail.tsx +++ b/frontend/plugins/kubepanel/src/pages/kubepanel/components/kube-object/storage/volume-claim/volume-claim-detail.tsx @@ -1,7 +1,7 @@ import { PersistentVolumeClaim, Pod } from '@/k8slens/kube-object'; import Drawer from '../../../drawer/drawer'; import { KubeObjectInfoList } from '@/components/kube/object/detail/kube-object-detail-info-list'; -import { DrawerItem } from '@/pages/kubepanel/components/drawer/drawer-item'; +import DrawerItem from '@/pages/kubepanel/components/drawer/drawer-item'; import { KubeBadge } from '@/components/kube/kube-badge'; import React from 'react'; import DrawerPanel from '../../../drawer/drawer-panel'; diff --git a/frontend/plugins/kubepanel/src/pages/kubepanel/components/kube-object/workload/deployment/deployment-detail.tsx b/frontend/plugins/kubepanel/src/pages/kubepanel/components/kube-object/workload/deployment/deployment-detail.tsx index ee49e323002..1fa8066d2d7 100644 --- a/frontend/plugins/kubepanel/src/pages/kubepanel/components/kube-object/workload/deployment/deployment-detail.tsx +++ b/frontend/plugins/kubepanel/src/pages/kubepanel/components/kube-object/workload/deployment/deployment-detail.tsx @@ -1,5 +1,5 @@ import { KubeBadge } from '@/components/kube/kube-badge'; -import { DrawerItem } from '@/pages/kubepanel/components/drawer/drawer-item'; +import DrawerItem from '@/pages/kubepanel/components/drawer/drawer-item'; import { KubeObjectInfoList } from '@/components/kube/object/detail/kube-object-detail-info-list'; import { Deployment } from '@/k8slens/kube-object'; import { getConditionColor } from '@/utils/condtion-color'; diff --git a/frontend/plugins/kubepanel/src/pages/kubepanel/components/kube-object/workload/overview/event-overview.tsx b/frontend/plugins/kubepanel/src/pages/kubepanel/components/kube-object/workload/overview/event-overview.tsx new file mode 100644 index 00000000000..9622ac8f1fb --- /dev/null +++ b/frontend/plugins/kubepanel/src/pages/kubepanel/components/kube-object/workload/overview/event-overview.tsx @@ -0,0 +1,93 @@ +import Title from '@/components/common/title/title'; +import { KubeObjectAge } from '@/components/kube/object/kube-object-age'; +import { ReactiveDuration } from '@/components/kube/reactive-duration'; +import { Resources } from '@/constants/kube-object'; +import { KubeEvent, ObjectReference } from '@/k8slens/kube-object'; +import { useEventStore } from '@/store/k8s/event.store'; +import { fetchData } from '@/store/kube'; +import { useQuery } from '@tanstack/react-query'; +import { Table, Tooltip } from 'antd'; +import { ColumnsType } from 'antd/lib/table'; + +const columns: ColumnsType = [ + { + title: Type, + key: 'type', + dataIndex: 'type', + render: (type: string | undefined) => { + if (!type) return 'Unknown'; + return type; + } + }, + { + title: Message, + key: 'message', + ellipsis: true, + dataIndex: 'message', + render: (message: string | undefined, event: KubeEvent) => { + if (!message) return ''; + + let renderedMessage: React.ReactNode; + switch (event.type) { + case 'Warning': + renderedMessage = {message}; + break; + case 'Normal': + default: + renderedMessage = message; + } + return {renderedMessage}; + } + }, + { + title: Involved Object, + key: 'involved-object', + dataIndex: 'involvedObject', + render: (involvedObject: Required | undefined) => { + if (!involvedObject) return ''; + return {involvedObject.name}; + } + }, + { + title: Source, + key: 'source', + render: (_, event: KubeEvent) => event.getSource() + }, + { + title: Count, + dataIndex: 'count', + key: 'count', + render: (count: number | undefined) => { + if (!count) return ''; + return count; + } + }, + { + title: Age, + key: 'age', + render: (_, event: KubeEvent) => { + return ; + } + }, + { + title: Last Seen, + key: 'last-seen', + render: (_, event: KubeEvent) => { + return ; + } + } +]; + +const EventOverview = () => { + const { items, replace } = useEventStore(); + + useQuery(['events'], () => fetchData(replace, Resources.Events), { + refetchInterval: 10000 + }); + + return ( +
+ ); +}; + +export default EventOverview; diff --git a/frontend/plugins/kubepanel/src/pages/kubepanel/components/kube-object/workload/overview/overview.tsx b/frontend/plugins/kubepanel/src/pages/kubepanel/components/kube-object/workload/overview/overview.tsx index af7eedff14b..a5bd84e2de6 100644 --- a/frontend/plugins/kubepanel/src/pages/kubepanel/components/kube-object/workload/overview/overview.tsx +++ b/frontend/plugins/kubepanel/src/pages/kubepanel/components/kube-object/workload/overview/overview.tsx @@ -14,6 +14,9 @@ import { useStatefulSetStore } from '@/store/kube'; import { Resources } from '@/constants/kube-object'; +import { Section } from '@/components/common/section/section'; +import Title from '@/components/common/title/title'; +import EventOverview from './event-overview'; const OverviewPage = () => { const requestController = useRef(new RequestController({ timeoutDuration: 10000 })); @@ -48,9 +51,17 @@ const OverviewPage = () => { })); return ( - -
Overview
- + +
+ Overview +
+
+ +
+
+ Events + +
); }; diff --git a/frontend/plugins/kubepanel/src/pages/kubepanel/components/kube-object/workload/pod/container-detail.tsx b/frontend/plugins/kubepanel/src/pages/kubepanel/components/kube-object/workload/pod/container-detail.tsx index 08b829d4d3a..8d7c05a9cae 100644 --- a/frontend/plugins/kubepanel/src/pages/kubepanel/components/kube-object/workload/pod/container-detail.tsx +++ b/frontend/plugins/kubepanel/src/pages/kubepanel/components/kube-object/workload/pod/container-detail.tsx @@ -1,7 +1,7 @@ import { Container, Pod } from '@/k8slens/kube-object'; import ContainerStatusBrick from './container-status-brick'; import { keys } from 'lodash'; -import { DrawerItem } from '@/pages/kubepanel/components/drawer/drawer-item'; +import DrawerItem from '@/pages/kubepanel/components/drawer/drawer-item'; import ContainerStatus, { ContainerLastState } from './container-status'; import { KubeBadge } from '@/components/kube/kube-badge'; import { Tooltip } from 'antd'; diff --git a/frontend/plugins/kubepanel/src/pages/kubepanel/components/kube-object/workload/pod/pod-detail.tsx b/frontend/plugins/kubepanel/src/pages/kubepanel/components/kube-object/workload/pod/pod-detail.tsx index d59811f49f2..bd6a6904475 100644 --- a/frontend/plugins/kubepanel/src/pages/kubepanel/components/kube-object/workload/pod/pod-detail.tsx +++ b/frontend/plugins/kubepanel/src/pages/kubepanel/components/kube-object/workload/pod/pod-detail.tsx @@ -1,4 +1,4 @@ -import { DrawerItem } from '@/pages/kubepanel/components/drawer/drawer-item'; +import DrawerItem from '@/pages/kubepanel/components/drawer/drawer-item'; import { KubeObjectInfoList } from '@/components/kube/object/detail/kube-object-detail-info-list'; import { Pod } from '@/k8slens/kube-object'; import { Tooltip } from 'antd'; diff --git a/frontend/plugins/kubepanel/src/pages/kubepanel/components/kube-object/workload/statefulset/statefulset-detail.tsx b/frontend/plugins/kubepanel/src/pages/kubepanel/components/kube-object/workload/statefulset/statefulset-detail.tsx index aa1f6ff3362..ce29df4cf2c 100644 --- a/frontend/plugins/kubepanel/src/pages/kubepanel/components/kube-object/workload/statefulset/statefulset-detail.tsx +++ b/frontend/plugins/kubepanel/src/pages/kubepanel/components/kube-object/workload/statefulset/statefulset-detail.tsx @@ -3,7 +3,7 @@ import Drawer from '../../../drawer/drawer'; import { KubeObjectInfoList } from '@/components/kube/object/detail/kube-object-detail-info-list'; import PodDetailTolerations from '../pod/pod-detail-tolerations'; import PodDetailAffinities from '../pod/pod-detail-affinities'; -import { DrawerItem } from '@/pages/kubepanel/components/drawer/drawer-item'; +import DrawerItem from '@/pages/kubepanel/components/drawer/drawer-item'; import PodDetailStatuses from '../pod/pod-detail-statuses'; import { KubeBadge } from '@/components/kube/kube-badge'; import DrawerPanel from '../../../drawer/drawer-panel'; diff --git a/frontend/plugins/kubepanel/src/pages/kubepanel/components/layout.tsx b/frontend/plugins/kubepanel/src/pages/kubepanel/components/layout.tsx index 3ee3baca765..061d26e0b48 100644 --- a/frontend/plugins/kubepanel/src/pages/kubepanel/components/layout.tsx +++ b/frontend/plugins/kubepanel/src/pages/kubepanel/components/layout.tsx @@ -17,7 +17,7 @@ export default function AppLayout({ children, onClickSideNavItem }: Props) { - {children} + {children} diff --git a/frontend/plugins/kubepanel/src/pages/kubepanel/components/modal/create-resource-modal.tsx b/frontend/plugins/kubepanel/src/pages/kubepanel/components/modal/create-resource-modal.tsx index 25f0ab852e5..c5bea759a1b 100644 --- a/frontend/plugins/kubepanel/src/pages/kubepanel/components/modal/create-resource-modal.tsx +++ b/frontend/plugins/kubepanel/src/pages/kubepanel/components/modal/create-resource-modal.tsx @@ -38,6 +38,16 @@ const options: Option[] = [ } ] }, + { + value: 'network', + label: 'Network', + children: [ + { + value: Resources.Ingresses, + label: 'Ingress' + } + ] + }, { value: 'config', label: 'Config', @@ -45,6 +55,10 @@ const options: Option[] = [ { value: Resources.ConfigMaps, label: 'Config Map' + }, + { + value: Resources.Secrets, + label: 'Secret' } ] }, diff --git a/frontend/plugins/kubepanel/src/pages/kubepanel/components/sidebar/sidebar.tsx b/frontend/plugins/kubepanel/src/pages/kubepanel/components/sidebar/sidebar.tsx index 6352147c41a..f1b17fb4107 100644 --- a/frontend/plugins/kubepanel/src/pages/kubepanel/components/sidebar/sidebar.tsx +++ b/frontend/plugins/kubepanel/src/pages/kubepanel/components/sidebar/sidebar.tsx @@ -1,10 +1,11 @@ import { DashboardOutlined, DatabaseOutlined, + GatewayOutlined, ReloadOutlined, SettingOutlined } from '@ant-design/icons'; -import { Button, ConfigProvider, Divider, Flex, Menu, MenuProps } from 'antd'; +import { Button, Flex, Menu, MenuProps } from 'antd'; type MenuItem = Required['items'][number]; @@ -13,8 +14,10 @@ export enum SideNavItemKey { Pod = 'pod', Deployment = 'deployment', ConfigMap = 'config-map', - PersistentVolumeClaim = 'pvc', - StatefulSet = 'stateful-set' + PersistentVolumeClaim = 'volume-claim', + StatefulSet = 'stateful-set', + Secret = 'secret', + Ingress = 'ingress' } function getItem( @@ -41,8 +44,10 @@ const items: MenuProps['items'] = [ getItem('Stateful Sets', SideNavItemKey.StatefulSet) ]), getItem('Config', 'config', , [ - getItem('Config Maps', SideNavItemKey.ConfigMap) + getItem('Config Maps', SideNavItemKey.ConfigMap), + getItem('Secrets', SideNavItemKey.Secret) ]), + getItem('Network', 'network', , [getItem('Ingress', SideNavItemKey.Ingress)]), getItem('Storage', 'storage', , [ getItem('Persistent Volume Claims', SideNavItemKey.PersistentVolumeClaim) ]) @@ -54,39 +59,25 @@ interface Props { const ResourceSideNav = ({ onClick = () => {} }: Props) => { return ( - - -
-
-
KubePanel
-
+ +
+
+
KubePanel
+
- onClick(key as SideNavItemKey)} - /> - - +
+ onClick(key as SideNavItemKey)} + /> + ); }; diff --git a/frontend/plugins/kubepanel/src/pages/kubepanel/index.tsx b/frontend/plugins/kubepanel/src/pages/kubepanel/index.tsx index a8e692fbab9..1b89f7aa771 100644 --- a/frontend/plugins/kubepanel/src/pages/kubepanel/index.tsx +++ b/frontend/plugins/kubepanel/src/pages/kubepanel/index.tsx @@ -10,6 +10,8 @@ import PersistentVolumeClaimOverviewPage from './components/kube-object/storage/ import { FloatButton } from 'antd'; import CreateResourceModal from './components/modal/create-resource-modal'; import { PlusOutlined } from '@ant-design/icons'; +import SecretOverviewPage from './components/kube-object/config/secret/secret'; +import IngressOverviewPage from './components/kube-object/network/ingress/ingress'; const switchPage = (key: SideNavItemKey): React.ReactNode => { switch (key) { @@ -25,6 +27,10 @@ const switchPage = (key: SideNavItemKey): React.ReactNode => { return ; case SideNavItemKey.PersistentVolumeClaim: return ; + case SideNavItemKey.Secret: + return ; + case SideNavItemKey.Ingress: + return ; default: return ; } @@ -39,6 +45,8 @@ const Home = () => { {switchPage(sideNavItemKey)} } + tooltip="Create a resource." + style={{ left: 24, bottom: 30, width: 48, height: 48 }} type="primary" onClick={() => setOpenCreateResourceModal(true)} /> diff --git a/frontend/plugins/kubepanel/src/services/backend/api.ts b/frontend/plugins/kubepanel/src/services/backend/api.ts index d62cdd85310..5ea341e2e47 100644 --- a/frontend/plugins/kubepanel/src/services/backend/api.ts +++ b/frontend/plugins/kubepanel/src/services/backend/api.ts @@ -3,7 +3,7 @@ import { merge } from 'lodash'; import { Options, get, post, delete as delete_, put } from 'request'; type ApiPrefix = 'api' | 'apis'; -type ApiGroup = 'apps'; +type ApiGroup = string; type ApiVersion = 'v1'; export interface KubeApiUrlParams { @@ -132,7 +132,6 @@ export const updateResource = async ( namespace: urlParams.namespace } }); - console.log(url); return new Promise((resolve, reject) => { try { put(url, { body: data, ...opts }, (error, response, body) => { diff --git a/frontend/plugins/kubepanel/src/store/k8s/event.store.ts b/frontend/plugins/kubepanel/src/store/k8s/event.store.ts new file mode 100644 index 00000000000..c3f09e518bf --- /dev/null +++ b/frontend/plugins/kubepanel/src/store/k8s/event.store.ts @@ -0,0 +1,8 @@ +import { KubeEvent } from '@/k8slens/kube-object'; +import { create } from 'zustand'; +import { createKubeStoreSlice } from './kube.store'; +import { EventStore } from '@/types/state'; + +export const useEventStore = create()((...a) => ({ + ...createKubeStoreSlice(KubeEvent.kind)(...a) +})); diff --git a/frontend/plugins/kubepanel/src/store/k8s/ingress.store.ts b/frontend/plugins/kubepanel/src/store/k8s/ingress.store.ts new file mode 100644 index 00000000000..19569cc66c9 --- /dev/null +++ b/frontend/plugins/kubepanel/src/store/k8s/ingress.store.ts @@ -0,0 +1,8 @@ +import { IngressStore } from '@/types/state'; +import { create } from 'zustand'; +import { createKubeStoreSlice } from './kube.store'; +import { Ingress } from '@/k8slens/kube-object'; + +export const useIngressStore = create()((...a) => ({ + ...createKubeStoreSlice(Ingress.kind)(...a) +})); diff --git a/frontend/plugins/kubepanel/src/store/k8s/secret.store.ts b/frontend/plugins/kubepanel/src/store/k8s/secret.store.ts new file mode 100644 index 00000000000..45f933c734a --- /dev/null +++ b/frontend/plugins/kubepanel/src/store/k8s/secret.store.ts @@ -0,0 +1,8 @@ +import { Secret } from '@/k8slens/kube-object'; +import { create } from 'zustand'; +import { createKubeStoreSlice } from './kube.store'; +import { SecretStore } from '@/types/state'; + +export const useSecretStore = create()((...a) => ({ + ...createKubeStoreSlice(Secret.kind)(...a) +})); diff --git a/frontend/plugins/kubepanel/src/store/kube.ts b/frontend/plugins/kubepanel/src/store/kube.ts index 8f7ce5e9a9f..7a06c6ae19a 100644 --- a/frontend/plugins/kubepanel/src/store/kube.ts +++ b/frontend/plugins/kubepanel/src/store/kube.ts @@ -3,4 +3,6 @@ export * from './k8s/deployment.store'; export * from './k8s/stateful-set.store'; export * from './k8s/config-map.store'; export * from './k8s/volume-claim.store'; +export * from './k8s/ingress.store'; +export * from './k8s/secret.store'; export * from './k8s/kube.store'; diff --git a/frontend/plugins/kubepanel/src/types/detail.d.ts b/frontend/plugins/kubepanel/src/types/detail.d.ts new file mode 100644 index 00000000000..f24a69588c3 --- /dev/null +++ b/frontend/plugins/kubepanel/src/types/detail.d.ts @@ -0,0 +1,7 @@ +import { KubeObject } from '@/k8slens/kube-object'; + +type DetailDrawerProps = { + obj?: K; + onClose: () => void; + open: boolean; +}; diff --git a/frontend/plugins/kubepanel/src/types/state.d.ts b/frontend/plugins/kubepanel/src/types/state.d.ts index a7b133b2940..9dde82e067e 100644 --- a/frontend/plugins/kubepanel/src/types/state.d.ts +++ b/frontend/plugins/kubepanel/src/types/state.d.ts @@ -1,33 +1,38 @@ import { - ConfigMap, - Deployment, - KubeObject, - PersistentVolumeClaim, - Pod, - StatefulSet - } from '@/k8slens/kube-object'; - - type KubeStoreState = { - items: Array; - kind: K['kind']; - isLoaded: boolean; - }; - type KubeStoreAction = { - modify: (item: K) => void; - remove: (item: K) => void; - replace: (items: K[]) => void; - setIsLoaded: (isLoaded: boolean) => void; - }; - - type KubeStore = KubeStoreState & KubeStoreAction; - - type StatusesComputed = { - getStatuses: Record; - }; - - type PodStore = KubeStore; - type DeploymentStore = KubeStore; - type StatefulSetStore = KubeStore; - type ConfigMapStore = KubeStore; - type VolumeClaimStore = KubeStore; - \ No newline at end of file + ConfigMap, + Deployment, + Ingress, + KubeEvent, + KubeObject, + PersistentVolumeClaim, + Pod, + Secret, + StatefulSet +} from '@/k8slens/kube-object'; + +type KubeStoreState = { + items: Array; + kind: K['kind']; + isLoaded: boolean; +}; +type KubeStoreAction = { + modify: (item: K) => void; + remove: (item: K) => void; + replace: (items: K[]) => void; + setIsLoaded: (isLoaded: boolean) => void; +}; + +type KubeStore = KubeStoreState & KubeStoreAction; + +type StatusesComputed = { + getStatuses: Record; +}; + +type PodStore = KubeStore; +type DeploymentStore = KubeStore; +type StatefulSetStore = KubeStore; +type ConfigMapStore = KubeStore; +type VolumeClaimStore = KubeStore; +type SecretStore = KubeStore; +type IngressStore = KubeStore; +type EventStore = KubeStore;