diff --git a/locales/en/l10n-projects-applicationWorkloads-routes-details.js b/locales/en/l10n-projects-applicationWorkloads-routes-details.js index 783b39dd01a..a843e7ce9ec 100644 --- a/locales/en/l10n-projects-applicationWorkloads-routes-details.js +++ b/locales/en/l10n-projects-applicationWorkloads-routes-details.js @@ -18,6 +18,6 @@ module.exports = { UNABLE_TO_ACCESS_TIP: 'Make sure that domain name resolution policies have been configured in your DNS server or the hosts file of your client machine.', CERTIFICATE_VALUE: 'Certificate: {value}', - // Metadata - // Events + ROUTE_PATH: 'Path', + ROUTE_PORT: 'Port', }; diff --git a/locales/es/l10n-projects-applicationWorkloads-routes-details.js b/locales/es/l10n-projects-applicationWorkloads-routes-details.js index 229a2ca062c..35d4954da1a 100644 --- a/locales/es/l10n-projects-applicationWorkloads-routes-details.js +++ b/locales/es/l10n-projects-applicationWorkloads-routes-details.js @@ -18,4 +18,6 @@ module.exports = { UNABLE_TO_ACCESS_TIP: 'Make sure that domain name resolution policies have been configured in your DNS server or the hosts file of your client machine.', CERTIFICATE_VALUE: 'Certificate: {value}', + ROUTE_PATH: 'Path', + ROUTE_PORT: 'Port', }; diff --git a/locales/tc/l10n-projects-applicationWorkloads-routes-details.js b/locales/tc/l10n-projects-applicationWorkloads-routes-details.js index bc62e42bfe9..42cac9a820b 100644 --- a/locales/tc/l10n-projects-applicationWorkloads-routes-details.js +++ b/locales/tc/l10n-projects-applicationWorkloads-routes-details.js @@ -18,4 +18,6 @@ module.exports = { UNABLE_TO_ACCESS_TIP: 'Make sure that domain name resolution policies have been configured in your DNS server or the hosts file of your client machine.', CERTIFICATE_VALUE: 'Certificate: {value}', + ROUTE_PATH: '路徑', + ROUTE_PORT: '端口', }; diff --git a/locales/zh/l10n-projects-applicationWorkloads-routes-details.js b/locales/zh/l10n-projects-applicationWorkloads-routes-details.js index 40b16e75b07..fc1ed5d16d4 100644 --- a/locales/zh/l10n-projects-applicationWorkloads-routes-details.js +++ b/locales/zh/l10n-projects-applicationWorkloads-routes-details.js @@ -17,4 +17,6 @@ module.exports = { UNABLE_TO_ACCESS: '无法访问服务', UNABLE_TO_ACCESS_TIP: '请确保已在您的 DNS 服务器或客户机 hosts 文件中配置域名解析规则。', CERTIFICATE_VALUE: '证书:{value}', + ROUTE_PATH: '路径', + ROUTE_PORT: '端口', }; diff --git a/packages/shared/src/components/Apps/Applications/AppTypeTable/OPAppTable/Ingresses/Item.tsx b/packages/shared/src/components/Apps/Applications/AppTypeTable/OPAppTable/Ingresses/Item.tsx index aa64e2197de..e74cd990f9d 100644 --- a/packages/shared/src/components/Apps/Applications/AppTypeTable/OPAppTable/Ingresses/Item.tsx +++ b/packages/shared/src/components/Apps/Applications/AppTypeTable/OPAppTable/Ingresses/Item.tsx @@ -5,70 +5,119 @@ import React, { useMemo } from 'react'; import { Link } from 'react-router-dom'; -import { Field } from '@kubed/components'; -import { getDisplayName } from '../../../../../../utils'; import Icon from '../../../../../Icon'; import { ItemWrapper } from '../styles'; -import { AccessButton, RulesWrapper } from './styles'; +import { AccessButton, Content, Path, Tip, Title } from './styles'; +import { find, get, isEmpty } from 'lodash'; +import { gatewayStore } from '../../../../../../stores'; +import { Col, Row, Tooltip } from '@kubed/components'; +const { useQueryNewGatewayByProject } = gatewayStore; type Props = { - detail: any; - gateway: any; + ingress: any; prefix?: string; + params: Record; }; -function IngressesItem({ prefix, detail, gateway }: Props): JSX.Element { - const tls = detail.tls || []; - const ports = useMemo(() => { - const finalPorts: Record = {}; - if (gateway && gateway.ports && gateway.type === 'NodePort') { - gateway.ports.forEach((_port: any) => { - if (_port.name === 'http' && _port.nodePort !== 80) { - finalPorts.http = _port.nodePort; - } - if (_port.name === 'https' && _port.nodePort !== 443) { - finalPorts.https = _port.nodePort; - } - }); - } +type Tls = { + hosts: string[]; + secretName: string; +}[]; + +function IngressesItem({ prefix, ingress, params }: Props): JSX.Element { + const { data } = useQueryNewGatewayByProject(params, { + enabled: !!params.cluster && !!params.workspace, + }); + const rules = get(ingress, 'spec.rules', []); + const ingressClassName = get(ingress, 'spec.ingressClassName', ''); - return finalPorts; - }, []); + const gateway = useMemo(() => { + return find(data, item => item.ingressClass === ingressClassName); + }, [data, ingressClassName]); + const tls: Tls = get(ingress, 'spec.tls', []); return ( - } - label={t('DOMAIN_NAME_VALUE', { - value: detail.rules.map((rule: any) => rule.host).join(', '), - })} - value={{getDisplayName(detail)}} - /> - - {detail.rules.map((rule: any) => { - const protocol = tls.hosts && tls.hosts.includes(rule.host) ? 'https' : 'http'; - let host = `${protocol}://${rule.host}`; - if (ports[protocol]) { - host = `${host}:${ports[protocol]}`; + {rules.map((rule: any) => { + const tlsItem = tls.find(item => item.hosts && item.hosts.includes(rule.host)); + const protocol = tlsItem ? 'https' : 'http'; + let host = rule.host; + const service = get(gateway, 'service', []); + if (!isEmpty(service)) { + const tempPort = find(service, item => item.name === protocol); + if ( + tempPort && + ((protocol === 'http' && tempPort.nodePort !== 80) || + (protocol === 'https' && tempPort.nodePort !== 443)) + ) { + host = `${host}:${tempPort.nodePort}`; } - - return rule.http.paths.map((path: any) => ( -
  • - URL:   - - - {host} - {path.path} - - - - {t('ACCESS_SERVICE')} - -
  • - )); - })} -
    + } + return ( + <> + + + + {host} + <div> + <span> + {t('PROTOCOL_VALUE')} + {protocol.toUpperCase()} + </span> + {protocol === 'https' && ( + <span> + {t('CERTIFICATE_VALUE')} + {tlsItem?.secretName} + </span> + )} + <Tip> + {t('UNABLE_TO_ACCESS')} +    + <Tooltip content={t('UNABLE_TO_ACCESS_TIP')}> + <Icon name="question" /> + </Tooltip> + </Tip> + </div> + + + {rule.http.paths.map((path: any, i: number) => ( + + + + + {t('ROUTE_PATH')}:{path.path} + + + + + {t('SERVICE_COLON')} + + + {path.backend.service.name} + + + + + + + {t('ROUTE_PORT')}:{path.backend.service.port.number} + + + + + {t('ACCESS_SERVICE')} + + + + + ))} + + ); + })}
    ); } diff --git a/packages/shared/src/components/Apps/Applications/AppTypeTable/OPAppTable/Ingresses/index.tsx b/packages/shared/src/components/Apps/Applications/AppTypeTable/OPAppTable/Ingresses/index.tsx index c93c2f9ffde..d326dcb41a3 100644 --- a/packages/shared/src/components/Apps/Applications/AppTypeTable/OPAppTable/Ingresses/index.tsx +++ b/packages/shared/src/components/Apps/Applications/AppTypeTable/OPAppTable/Ingresses/index.tsx @@ -3,68 +3,48 @@ * https://github.com/kubesphere/console/blob/master/LICENSE */ -import React, { useMemo } from 'react'; -import { isEmpty } from 'lodash'; -import { useQueries } from 'react-query'; +import React, { useEffect, useMemo, useState } from 'react'; -import { joinSelector } from '../../../../../../utils'; -import { gatewayStore } from '../../../../../../stores'; import IngressesItem from './Item'; import ResourceCard from '../ResourceCard'; -import { openpitrixStore } from '../../../../../../stores'; -const { useBaseK8sList } = openpitrixStore; - -const { fetchDetail } = gatewayStore; +import { EmptyTips } from './styles'; type Props = { - cluster?: string; - namespace?: string; - prefix?: string; - title?: string; - selector?: any; -}; - -function Ingresses({ title, cluster, namespace, selector, prefix }: Props): JSX.Element { - const k8sParams = useMemo(() => { - if (!isEmpty(selector)) { - return { - cluster, - namespace, - labelSelector: joinSelector(selector), - }; - } + detail: any; - return {}; - }, [selector]); - const gateWays = useQueries([ - { - queryKey: ['gateWay', 'cluster', cluster], - queryFn: () => fetchDetail({ cluster }), - enabled: !!cluster, - }, - { - queryKey: ['gateWay', 'namespace', cluster, namespace], - queryFn: () => fetchDetail({ cluster, namespace }), - enabled: !!cluster && !!namespace, - }, - ]); - const { data } = useBaseK8sList({ - module: 'ingresses', - params: k8sParams, - autoFetch: gateWays[0].isSuccess && gateWays[1].isSuccess, - }); + params: Record; +}; +function Ingresses({ detail, params }: Props): JSX.Element { + const { workspace, cluster, namespace } = params; + const prefix = workspace + ? `/${workspace}/clusters/${cluster}/projects/${namespace}` + : `/clusters/${cluster}/projects/${namespace}`; + const [ingressList, setIngressList] = useState([]); + + const list = useMemo( + () => + detail.map((item: any) => ({ + ...item, + module: item.kind, + ...item.metadata, + })), + [detail], + ); + useEffect(() => { + if (!list) return; + const ingressItems = list.filter((item: any) => item.kind === 'Ingress'); + setIngressList(ingressItems); + }, [list]); return ( {t('NO_AVAILABLE_RESOURCE_VALUE', { resource: t('ROUTE_PL') })} + } itemRender={item => ( - + )} /> ); diff --git a/packages/shared/src/components/Apps/Applications/AppTypeTable/OPAppTable/Ingresses/styles.ts b/packages/shared/src/components/Apps/Applications/AppTypeTable/OPAppTable/Ingresses/styles.ts index 44055b3a2fb..a1d7070fdae 100644 --- a/packages/shared/src/components/Apps/Applications/AppTypeTable/OPAppTable/Ingresses/styles.ts +++ b/packages/shared/src/components/Apps/Applications/AppTypeTable/OPAppTable/Ingresses/styles.ts @@ -11,6 +11,9 @@ export const AccessButton = styled(Button)` top: 50%; right: 6px; transform: translateY(-50%); + & span { + color: #242e42 !important; + } `; export const RulesWrapper = styled.ul` @@ -28,3 +31,83 @@ export const RulesWrapper = styled.ul` } } `; + +export const EmptyTips = styled.div` + padding: 20px; + color: ${({ theme }) => theme.palette.accents_4}; + font-size: 16px; + font-weight: 600; + line-height: 1.5; + background-color: ${({ theme }) => theme.palette.accents_0}; + border-radius: 4px; +`; + +export const CardWrapper = styled.div` + padding: 20px; + border-radius: 4px; + background-color: #f9fbfd; + transition: all 0.3s ease-in-out; + + & + & { + margin-top: 8px; + } +`; + +export const Content = styled.div` + position: relative; + margin-bottom: 12px; + + & > svg { + vertical-align: middle; + } +`; + +export const Title = styled.div` + display: inline-block; + margin-left: 20px; + margin-right: 20px; + font-weight: 600; + line-height: 1.43; + color: #242e42; + vertical-align: middle; + + & > div { + font-size: 12px; + color: #79879c; + font-weight: 400; + + span + span { + margin-left: 100px; + } + } +`; +export const Tip = styled.div` + position: absolute; + top: 24px; + right: 12px; + display: inline-flex; + align-items: center; + justify-content: center; +`; + +export const Path = styled.div` + position: relative; + padding: 12px 20px; + border-radius: 22px; + background-color: #eff4f9; + border: solid 1px #ccd3db; + + & + & { + margin-top: 8px; + } + + span { + text-align: left; + color: #79879c; + + strong { + line-height: 1.43; + color: #242e42; + } + } +`; diff --git a/packages/shared/src/components/Apps/Applications/AppTypeTable/OPAppTable/Volumes/Item.tsx b/packages/shared/src/components/Apps/Applications/AppTypeTable/OPAppTable/Volumes/Item.tsx new file mode 100644 index 00000000000..333fa088ccd --- /dev/null +++ b/packages/shared/src/components/Apps/Applications/AppTypeTable/OPAppTable/Volumes/Item.tsx @@ -0,0 +1,59 @@ +/* + * This file is part of KubeSphere Console. + * Copyright (C) 2019 The KubeSphere Console Authors. + * + * KubeSphere Console is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KubeSphere Console is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with KubeSphere Console. If not, see . + */ + +import { get } from 'lodash'; +import React from 'react'; +import { Link } from 'react-router-dom'; + +import { Text } from '@ks-console/shared'; + +import { Item } from './styles'; + +type Props = { + volume: any; + prefix?: string; +}; +const Card = ({ volume, prefix }: Props) => { + const description = t('STORAGE_CLASS_VALUE', { + value: get(volume, 'spec.storageClassName', '-'), + }); + const details = [ + { + title: get(volume, 'spec.resources.requests.storage', '-'), + description: t('CAPACITY'), + }, + { + title: get(volume, 'spec.accessModes[0]', '-'), + description: t('ACCESS_MODE_TCAP'), + }, + ]; + return ( + + {volume.name}} + description={description} + /> + {details.map((params, index) => ( + + ))} + + ); +}; + +export default Card; diff --git a/packages/shared/src/components/Apps/Applications/AppTypeTable/OPAppTable/Volumes/index.tsx b/packages/shared/src/components/Apps/Applications/AppTypeTable/OPAppTable/Volumes/index.tsx new file mode 100644 index 00000000000..8bfa55f36d5 --- /dev/null +++ b/packages/shared/src/components/Apps/Applications/AppTypeTable/OPAppTable/Volumes/index.tsx @@ -0,0 +1,60 @@ +/* + * This file is part of KubeSphere Console. + * Copyright (C) 2019 The KubeSphere Console Authors. + * + * KubeSphere Console is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KubeSphere Console is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with KubeSphere Console. If not, see . + */ + +import React, { useEffect, useMemo, useState } from 'react'; +import ResourceCard from '../ResourceCard'; +import VolumeItem from './Item'; +import { EmptyTips } from './styles'; + +interface VolumesCardProps { + prefix?: string; + detail: any[]; +} + +const VolumesCard: React.FC = ({ detail, prefix }) => { + const [volumeList, setVolumeList] = useState([]); + const list = useMemo( + () => + detail.map(item => ({ + ...item, + module: item.kind, + ...item.metadata, + })), + [detail], + ); + useEffect(() => { + if (!list) return; + const volumeItems = list.filter(item => item.kind === 'PersistentVolumeClaim'); + setVolumeList(volumeItems); + }, [list]); + return ( + + {t('NO_AVAILABLE_RESOURCE_VALUE', { resource: t('PERSISTENT_VOLUME_CLAIM') })} + + } + itemRender={item => } + /> + ); +}; + +export default VolumesCard; diff --git a/packages/shared/src/components/Apps/Applications/AppTypeTable/OPAppTable/Volumes/styles.ts b/packages/shared/src/components/Apps/Applications/AppTypeTable/OPAppTable/Volumes/styles.ts new file mode 100644 index 00000000000..178a7556c50 --- /dev/null +++ b/packages/shared/src/components/Apps/Applications/AppTypeTable/OPAppTable/Volumes/styles.ts @@ -0,0 +1,39 @@ +import styled from 'styled-components'; + +export const Content = styled.div` + padding: 12px; + border-radius: 4px; + background-color: ${({ theme }) => theme.palette.background}; +`; + +export const Item = styled.div` + display: flex; + padding: 12px; + margin-bottom: 8px; + background-color: ${({ theme }) => theme.palette.background}; + border: solid 1px ${({ theme }) => theme.palette.border}; + border-radius: 4px; + + &:last-child { + margin-bottom: 0; + } + + & > div { + margin-right: 12px; + min-width: 20%; + + &:first-child { + min-width: 35%; + } + } +`; + +export const EmptyTips = styled.div` + padding: 20px; + color: ${({ theme }) => theme.palette.accents_4}; + font-size: 16px; + font-weight: 600; + line-height: 1.5; + background-color: ${({ theme }) => theme.palette.accents_0}; + border-radius: 4px; +`; diff --git a/packages/shared/src/components/Apps/Applications/AppTypeTable/OPAppTable/Workloads/index.tsx b/packages/shared/src/components/Apps/Applications/AppTypeTable/OPAppTable/Workloads/index.tsx index 3dc577204bc..f5868b7e9be 100644 --- a/packages/shared/src/components/Apps/Applications/AppTypeTable/OPAppTable/Workloads/index.tsx +++ b/packages/shared/src/components/Apps/Applications/AppTypeTable/OPAppTable/Workloads/index.tsx @@ -3,7 +3,7 @@ * https://github.com/kubesphere/console/blob/master/LICENSE */ -import React, { useState, useEffect } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import WorkloadItem, { Detail } from './Item'; import ResourceCard from '../ResourceCard'; import { EmptyTips } from '../styles'; @@ -20,18 +20,28 @@ function Workloads({ detail, params }: Props): JSX.Element | null { ? `/${workspace}/clusters/${cluster}/projects/${namespace}` : `/clusters/${cluster}/projects/${namespace}`; const [selectItem, setSelectItem] = useState([]); - const [listMap, setListMap] = useState([]); - const list = detail.map(item => ({ - ...item, - module: item.kind, - ...item.metadata, - })); + const [deploymentList, setDeploymentList] = useState([]); + const [statefulSetList, setStatefulSetList] = useState([]); + const [daemonsetList, setDaemonsetList] = useState([]); + const list = useMemo( + () => + detail.map(item => ({ + ...item, + module: item.kind, + ...item.metadata, + })), + [detail], + ); useEffect(() => { if (!list) return; const deploymentItems = list.filter(item => item.kind === 'Deployment'); - setListMap(deploymentItems); - }, []); + const statefulSetItems = list.filter(item => item.kind === 'StatefulSet'); + const daemonsetItems = list.filter(item => item.kind === 'DaemonSet'); + setDeploymentList(deploymentItems); + setStatefulSetList(statefulSetItems); + setDaemonsetList(daemonsetItems); + }, [list]); function handleWorkloadItem(item: Detail, check: boolean) { const val = selectItem?.filter(workload => workload.name !== item.name); @@ -42,28 +52,57 @@ function Workloads({ detail, params }: Props): JSX.Element | null { } function itemRender(item: any) { + const URL_MAP = { + Deployment: 'deployments', + StatefulSet: 'statefulsets', + DaemonSet: 'daemonsets', + }; + const url = URL_MAP[item.kind as keyof typeof URL_MAP]; return ( select.name === item.name)} setSelectItem={val => handleWorkloadItem(item, !!val)} key={`${item.module}-${item.name}`} - prefix={`${prefix}/deployments`} + prefix={`${prefix}/${url}`} detail={item} /> ); } - if (!listMap.length) return null; return ( <> - {t('NO_AVAILABLE_RESOURCE_VALUE', { resource: t('WORKLOAD') })} - } - itemRender={item => itemRender(item)} - /> + {deploymentList.length > 0 && ( + {t('NO_AVAILABLE_RESOURCE_VALUE', { resource: t('WORKLOAD') })} + } + itemRender={item => itemRender(item)} + /> + )} + {statefulSetList.length > 0 && ( + {t('NO_AVAILABLE_RESOURCE_VALUE', { resource: t('WORKLOAD') })} + } + itemRender={item => itemRender(item)} + /> + )} + {daemonsetList.length > 0 && ( + {t('NO_AVAILABLE_RESOURCE_VALUE', { resource: t('WORKLOAD') })} + } + itemRender={item => itemRender(item)} + /> + )} ); } diff --git a/packages/shared/src/components/Apps/Applications/DetailInfo/ResourceStatus/index.tsx b/packages/shared/src/components/Apps/Applications/DetailInfo/ResourceStatus/index.tsx index 45b67028ccb..ba942a2f91a 100644 --- a/packages/shared/src/components/Apps/Applications/DetailInfo/ResourceStatus/index.tsx +++ b/packages/shared/src/components/Apps/Applications/DetailInfo/ResourceStatus/index.tsx @@ -12,6 +12,8 @@ import PodWorkloads from '../../AppTypeTable/OPAppTable/PodWorkloads'; import EdgeWorkloads from '../../AppTypeTable/OPAppTable/EdgeWorkloads'; import Services from '../../AppTypeTable/OPAppTable/Services'; import Workloads from '../../AppTypeTable/OPAppTable/Workloads'; +import Ingresses from '../../AppTypeTable/OPAppTable/Ingresses'; +import Volumes from '../../AppTypeTable/OPAppTable/Volumes'; const ResourceStatusWrapper = styled.div` & > div { @@ -43,9 +45,11 @@ function ResourceStatus(): JSX.Element { ) : ( <> - - - + + + + + )} diff --git a/packages/shared/src/stores/gateway.ts b/packages/shared/src/stores/gateway.ts index c3d37cbc8fa..aa908c8bcc0 100644 --- a/packages/shared/src/stores/gateway.ts +++ b/packages/shared/src/stores/gateway.ts @@ -303,6 +303,40 @@ const useBatchDeleteMutation = (options?: MutationOptions) => { ); }; +const getUrlParams = ({ workspace, namespace, cluster }: PathParams) => { + return workspace && !namespace + ? { workspace, cluster } + : { namespace: namespace || 'kubesphere-system', cluster }; +}; + +const getIngressUrl = ({ cluster, namespace, workspace }: PathParams) => { + const pathParams = getUrlParams({ workspace, namespace, cluster }); + + return `kapis/gateway.kubesphere.io/v1alpha2${getPath(pathParams)}/availableingressclassscopes`; +}; + +const ingressClassScopesMapper = (item: Record) => ({ + ...getBaseInfo(item), + name: get(item, 'metadata.name'), + ingressClass: get(item, 'spec.ingressClass.name'), + service: get(item, 'status.service'), + ingressIp: get(item, 'status.loadBalancer.ingress[0].ip', ''), + _originData: item, +}); + +const useQueryNewGatewayByProject = (params: PathParams, options: Record) => { + const url = getIngressUrl(params); + + return useQuery[]>({ + queryKey: [module, 'Ingress', params], + queryFn: () => request.get(url), + select: data => { + return data?.map(ingressClassScopesMapper); + }, + ...options, + }); +}; + const store = BaseStore({ module, apiVersion, @@ -335,4 +369,5 @@ export default { del, batchDelete, useBatchDeleteMutation, + useQueryNewGatewayByProject, };