diff --git a/console-extensions.json b/console-extensions.json index 2f98a85..21033fa 100644 --- a/console-extensions.json +++ b/console-extensions.json @@ -3,16 +3,8 @@ "type": "console.page/route", "properties": { "exact": true, - "path": "/k8s/ns/:ns/kuadrant/dashboard", - "component": { "$codeRef": "KuadrantDashboardPage" } - } - }, - { - "type": "console.page/route", - "properties": { - "exact": true, - "path": "/k8s/all-namespaces/kuadrant/dashboard", - "component": { "$codeRef": "KuadrantDashboardPage" } + "path": "/kuadrant/overview", + "component": { "$codeRef": "KuadrantOverviewPage" } } }, { @@ -70,9 +62,9 @@ { "type": "console.navigation/href", "properties": { - "id": "rhcl-dashboard-admin", - "name": "%plugin__console-plugin-template~Dashboard%", - "href": "/k8s/all-namespaces/kuadrant/dashboard", + "id": "kuadrant-overview-admin", + "name": "%plugin__console-plugin-template~Overview%", + "href": "/kuadrant/overview", "perspective": "admin", "section": "kuadrant-section-admin" } @@ -80,7 +72,7 @@ { "type": "console.navigation/href", "properties": { - "id": "rhcl-policies-admin", + "id": "kuadrant-policies-admin", "name": "%plugin__console-plugin-template~Policies%", "href": "/kuadrant/all-namespaces/policies", "perspective": "admin", @@ -90,7 +82,7 @@ { "type": "console.navigation/href", "properties": { - "id": "rhcl-policy-topology-admin", + "id": "kuadrant-policy-topology-admin", "name": "%plugin__console-plugin-template~Policy Topology%", "href": "/kuadrant/policy-topology", "perspective": "admin", @@ -109,9 +101,9 @@ { "type": "console.navigation/href", "properties": { - "id": "rhcl-dashboard-dev", - "name": "%plugin__console-plugin-template~Dashboard%", - "href": "/k8s/all-namespaces/kuadrant/dashboard", + "id": "kuadrant-dashboard-dev", + "name": "%plugin__console-plugin-template~Overview%", + "href": "/kuadrant/overview", "perspective": "dev", "section": "kuadrant-section-dev" } @@ -119,7 +111,7 @@ { "type": "console.navigation/href", "properties": { - "id": "rhcl-policies-dev", + "id": "kuadrant-policies-dev", "name": "%plugin__console-plugin-template~Policies%", "href": "/kuadrant/all-namespaces/policies", "perspective": "dev", @@ -129,7 +121,7 @@ { "type": "console.navigation/href", "properties": { - "id": "rhcl-policy-topology-dev", + "id": "kuadrant-policy-topology-dev", "name": "%plugin__console-plugin-template~Policy Topology%", "href": "/kuadrant/policy-topology", "perspective": "dev", diff --git a/locales/en/plugin__console-plugin-template.json b/locales/en/plugin__console-plugin-template.json index 30a2a4a..3847023 100644 --- a/locales/en/plugin__console-plugin-template.json +++ b/locales/en/plugin__console-plugin-template.json @@ -16,7 +16,7 @@ "Namespace": "Namespace", "Select a Namespace": "Select a Namespace", "Address": "Address", - "Dashboard": "Dashboard", + "Overview": "Overview", "Policies": "Policies", "Policy Topology": "Policy Topology", "TLS": "TLS", @@ -69,5 +69,24 @@ "Target reference type": "Target reference type", "Unique name of the RateLimitPolicy": "Unique name of the RateLimitPolicy", "RateLimitPolicy enables rate limiting for service workloads in a Gateway API network": "RateLimitPolicy enables rate limiting for service workloads in a Gateway API network", - "To set defaults, overrides, and more complex limits, use the YAML view.": "To set defaults, overrides, and more complex limits, use the YAML view." + "To set defaults, overrides, and more complex limits, use the YAML view.": "To set defaults, overrides, and more complex limits, use the YAML view.", + "Learn how to create, import and use Kuadrant policies on OpenShift with step-by-step instructions and tasks.": "Learn how to create, import and use Kuadrant policies on OpenShift with step-by-step instructions and tasks.", + "Learning Resources": "Learning Resources", + "Create Policies in": "Create Policies in", + "Add a new Gateway": "Add a new Gateway", + "View Documentation": "View Documentation", + "View all quick starts": "View all quick starts", + "Feature Highlights": "Feature Highlights", + "Read about the latest information and key features in the Kuadrant highlights.": "Read about the latest information and key features in the Kuadrant highlights.", + "highlights": "highlights", + "Visit the blog": "Visit the blog", + "Getting started resources": "Getting started resources", + "6 min read": "6 min read", + "Release Notes": "Release Notes", + "Enhance Your Work": "Enhance Your Work", + "Ease operational complexity with API management and App Connectivity by using additional Operators and tools.": "Ease operational complexity with API management and App Connectivity by using additional Operators and tools.", + "API Designer": "API Designer", + "cert-manager Operator": "cert-manager Operator", + "APIs / HTTPRoutes": "APIs / HTTPRoutes", + "Hide for session": "Hide for session" } diff --git a/package.json b/package.json index e0b8a95..3e03c8e 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "@openshift-console/dynamic-plugin-sdk": "^1.6.0", "@openshift-console/dynamic-plugin-sdk-webpack": "^1.2.0", "@patternfly/react-core": "^5.3.3", - "@patternfly/react-icons": "^5.1.1", + "@patternfly/react-icons": "^5.4.0", "@patternfly/react-styles": "^5.1.1", "@types/node": "^18.0.0", "@types/react": "^17.0.37", @@ -74,7 +74,7 @@ "displayName": "OpenShift Console Plugin Template", "description": "Template project for OpenShift Console plugins. Edit package.json to change this message and the plugin name.", "exposedModules": { - "KuadrantDashboardPage": "./components/KuadrantDashboardPage", + "KuadrantOverviewPage": "./components/KuadrantOverviewPage", "PolicyTopologyPage": "./components/PolicyTopologyPage", "KuadrantPoliciesPage": "./components/KuadrantPoliciesPage", "KuadrantDNSPolicyCreatePage": "./components/KuadrantDNSPolicyCreatePage", diff --git a/src/components/DropdownWithKebab.tsx b/src/components/DropdownWithKebab.tsx new file mode 100644 index 0000000..e699fa8 --- /dev/null +++ b/src/components/DropdownWithKebab.tsx @@ -0,0 +1,96 @@ +import * as React from 'react'; +import { EllipsisVIcon } from '@patternfly/react-icons'; +import { Dropdown, DropdownItem, DropdownList, MenuToggle, MenuToggleElement, Button, ButtonVariant } from '@patternfly/react-core'; +import { Modal, ModalBody, ModalFooter, ModalHeader } from '@patternfly/react-core/next'; +import { k8sDelete, K8sResourceCommon } from '@openshift-console/dynamic-plugin-sdk'; +import getModelFromResource from '../utils/getModelFromResource'; // Assume you have a utility for getting the model from the resource + +type DropdownWithKebabProps = { + obj: K8sResourceCommon; +}; + +const DropdownWithKebab: React.FC = ({ obj }) => { + const [isOpen, setIsOpen] = React.useState(false); + const [isDeleteModalOpen, setIsDeleteModalOpen] = React.useState(false); + + const onToggleClick = () => { + setIsOpen(!isOpen); + }; + + const onDeleteConfirm = async () => { + try { + const model = getModelFromResource(obj); + await k8sDelete({ model, resource: obj }); + console.log('Successfully deleted', obj.metadata.name); + } catch (error) { + console.error('Failed to delete', obj.metadata.name, error); + } finally { + setIsDeleteModalOpen(false); + } + }; + + const onDeleteClick = () => { + setIsDeleteModalOpen(true); + }; + + const onSelect = (_event: React.MouseEvent | undefined, value: string | number | undefined) => { + setIsOpen(false); + if (value === 'delete') { + onDeleteClick(); + } + }; + + return ( + <> + setIsOpen(isOpen)} + toggle={(toggleRef: React.Ref) => ( + + + + )} + shouldFocusToggleOnSelect + > + + + Edit + + + Delete + + + + + setIsDeleteModalOpen(false)} + aria-labelledby="delete-modal-title" + aria-describedby="delete-modal-body" + variant="medium" + > + + + Are you sure you want to delete the resource {obj.metadata.name}? + + + + + + + + ); +}; + +export default DropdownWithKebab; diff --git a/src/components/KuadrantDashboardPage.tsx b/src/components/KuadrantDashboardPage.tsx deleted file mode 100644 index 1933606..0000000 --- a/src/components/KuadrantDashboardPage.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import * as React from 'react'; -import { useParams } from 'react-router-dom'; -import Helmet from 'react-helmet'; -import { useTranslation } from 'react-i18next'; -import { - Page, - PageSection, - Title, -} from '@patternfly/react-core'; -import { - Table, - Thead, - Tr, - Th, - Td, - Tbody, -} from '@patternfly/react-table'; -import { useK8sWatchResource, K8sResourceCommon, ResourceLink, useActiveNamespace } from '@openshift-console/dynamic-plugin-sdk'; -import './kuadrant.css'; - -interface Resource { - name: string; - gvk: { - group: string; - version: string; - kind: string; - }; -} - -interface ExtendedK8sResourceCommon extends K8sResourceCommon { - status?: { - addresses?: { value: string }[]; - }; -} - -const resources: Resource[] = [ - { name: 'Gateways', gvk: { group: 'gateway.networking.k8s.io', version: 'v1', kind: 'Gateway' } }, - { name: 'AuthPolicies', gvk: { group: 'kuadrant.io', version: 'v1beta2', kind: 'AuthPolicy' } }, - { name: 'DNSPolicies', gvk: { group: 'kuadrant.io', version: 'v1alpha1', kind: 'DNSPolicy' } }, - { name: 'DNSRecords', gvk: { group: 'kuadrant.io', version: 'v1alpha1', kind: 'DNSRecord' } }, - { name: 'Kuadrants', gvk: { group: 'kuadrant.io', version: 'v1beta1', kind: 'Kuadrant' } }, - { name: 'ManagedZones', gvk: { group: 'kuadrant.io', version: 'v1alpha1', kind: 'ManagedZone' } }, - { name: 'RateLimitPolicies', gvk: { group: 'kuadrant.io', version: 'v1beta2', kind: 'RateLimitPolicy' } }, - { name: 'TLSPolicies', gvk: { group: 'kuadrant.io', version: 'v1alpha1', kind: 'TLSPolicy' } }, -]; - -const KuadrantDashboardPage: React.FC = () => { - const { t } = useTranslation('plugin__console-plugin-template'); - const { ns } = useParams<{ ns: string }>(); - const [activeNamespace, setActiveNamespace] = useActiveNamespace(); - - React.useEffect(() => { - if (ns && ns !== activeNamespace) { - setActiveNamespace(ns); - } - console.log(`Initial namespace: ${activeNamespace}`); - }, [ns, activeNamespace, setActiveNamespace]); - - const formatTimestamp = (timestamp: string) => { - const date = new Date(timestamp); - const now = new Date(); - const diff = now.getTime() - date.getTime(); - const diffDays = Math.floor(diff / (1000 * 3600 * 24)); - return diffDays > 0 ? `${diffDays}d` : 'Today'; - }; - - const renderTable = (resource: Resource, data: ExtendedK8sResourceCommon[], loaded: boolean, loadError: any) => ( - - {resource.name} - {loaded && !loadError ? ( - - - - - - - {resource.name === 'Gateways' && } - - - - {data.map((item) => ( - - - - - {resource.name === 'Gateways' && ( - - )} - - ))} - -
{t('Name')}{t('Namespace')}{t('Age')}{t('Address')}
- - - - {formatTimestamp(item.metadata.creationTimestamp)} - {item.status?.addresses?.length ? item.status.addresses.map((address) => address.value).join(', ') : 'N/A'} -
- ) : ( -
{t('Loading...')}
- )} -
- ); - - return ( - <> - - {t('Kuadrant')} - - - - {t('Kuadrant')} - - {resources.map((resource) => { - const { group, version, kind } = resource.gvk; - const [data, loaded, loadError] = useK8sWatchResource({ - groupVersionKind: { group, version, kind }, - namespace: activeNamespace === '#ALL_NS#' ? undefined : activeNamespace, - isList: true, - }); - - return renderTable(resource, data, loaded, loadError); - })} - - - ); -}; - -export default KuadrantDashboardPage; diff --git a/src/components/KuadrantOverviewPage.tsx b/src/components/KuadrantOverviewPage.tsx new file mode 100644 index 0000000..d7f2b84 --- /dev/null +++ b/src/components/KuadrantOverviewPage.tsx @@ -0,0 +1,276 @@ +import * as React from 'react'; +import { useParams } from 'react-router-dom'; +import Helmet from 'react-helmet'; +import { useTranslation } from 'react-i18next'; +import { + Page, + PageSection, + Title, + Card, + CardTitle, + CardBody, + CardExpandableContent, + CardHeader, + Flex, + FlexItem, + Text, + Stack, + StackItem, + Divider, + Dropdown, + DropdownItem, + DropdownList, + MenuToggle, +} from '@patternfly/react-core'; +import { + GlobeIcon, + ReplicatorIcon, + OptimizeIcon, + ArrowRightIcon, + ExternalLinkAltIcon, + EllipsisVIcon, +} from '@patternfly/react-icons'; +import { useActiveNamespace } from '@openshift-console/dynamic-plugin-sdk'; +import './kuadrant.css'; +import ResourceList from './ResourceList'; +import { sortable } from '@patternfly/react-table'; +import { INTERNAL_LINKS, EXTERNAL_LINKS } from '../constants/links'; + +const KuadrantOverviewPage: React.FC = () => { + const { t } = useTranslation('plugin__console-plugin-template'); + const { ns } = useParams<{ ns: string }>(); + const [activeNamespace, setActiveNamespace] = useActiveNamespace(); + const [isExpanded, setIsExpanded] = React.useState(true); + const [isOpen, setIsOpen] = React.useState(false); + const [hideCard, setHideCard] = React.useState(sessionStorage.getItem('hideGettingStarted') === 'true'); + + React.useEffect(() => { + if (ns && ns !== activeNamespace) { + setActiveNamespace(ns); + } + }, [ns, activeNamespace, setActiveNamespace]); + + const handleHideCard = () => { + setHideCard(true); + sessionStorage.setItem('hideGettingStarted', 'true'); + }; + + const onSelect = () => { + setIsOpen(!isOpen); + }; + + const dropdownItems = ( + <> + + {t('Hide for session')} + + + ); + + const headerActions = ( + <> + ( + setIsOpen(!isOpen)} + variant="plain" + aria-label="Card actions" + > + + )} + isOpen={isOpen} + onOpenChange={(isOpen: boolean) => setIsOpen(isOpen)} + > + {dropdownItems} + + + ); + + const columns = [{ + title: t('plugin__console-plugin-template~Name'), + id: 'name', + sort: 'metadata.name', + transforms: [sortable], + }, { + title: t('plugin__console-plugin-template~Namespace'), + id: 'namespace', + sort: 'metadata.namespace', + transforms: [sortable], + }, { + title: '', + id: 'kebab', + props: { className: 'pf-v5-c-table__action' }, + }]; + + return ( + <> + + {t('Kuadrant')} + + + + {t('Kuadrant')} Overview +
+ + {!hideCard && ( + + setIsExpanded(!isExpanded)} + toggleButtonProps={{ + 'aria-label': isExpanded ? t('Collapse Getting Started') : t('Expand Getting Started'), + }} + > + {t('Getting started resources')} + + + + + + + <GlobeIcon /> {t('Learning Resources')} + +

{t('Learn how to create, import and use Kuadrant policies on OpenShift with step-by-step instructions and tasks.')}

+ + + + {t('Create Policies in')} {t('Kuadrant')} + + + + + {t('Add a new Gateway')} + + + + + {t('View Documentation')} + + + + + {t('View all quick starts')} + + + +
+ + + + <OptimizeIcon /> {t('Feature Highlights')} + +

{t('Read about the latest information and key features in the Kuadrant highlights.')}

+ + + + {t('Kuadrant')} {t('highlights')}   + + + + + {t('Kuadrant')} {t('Release Notes')} + {t('6 min read')} + + + + + + {t('Visit the blog')} + + + +
+ + + + <ReplicatorIcon /> {t('Enhance Your Work')} + +

{t('Ease operational complexity with API management and App Connectivity by using additional Operators and tools.')}

+ + + + {t('API Designer')} + + + + + Observability for {t('Kuadrant')} + + + + + {t('cert-manager Operator')} + + + +
+
+
+
+
+ )} + + + + + + {t('Policies')} + + + + + + + + + + + + {t('Gateways')} + + + + + + + + {t('APIs / HTTPRoutes')} + + + + + + +
+
+ + ); +}; + +export default KuadrantOverviewPage; diff --git a/src/components/KuadrantPoliciesPage.tsx b/src/components/KuadrantPoliciesPage.tsx index 18fe008..ff24d3a 100644 --- a/src/components/KuadrantPoliciesPage.tsx +++ b/src/components/KuadrantPoliciesPage.tsx @@ -1,44 +1,22 @@ import * as React from 'react'; import { useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { - Dropdown, - DropdownItem, - DropdownList, - MenuToggle, - MenuToggleElement, - Alert, - AlertGroup, - Title, - Button, - ButtonVariant, -} from '@patternfly/react-core'; import { sortable } from '@patternfly/react-table'; -import { EllipsisVIcon } from '@patternfly/react-icons'; -import { Modal, ModalBody, ModalFooter, ModalHeader } from '@patternfly/react-core/next'; - import { - k8sDelete, - useK8sWatchResource, - K8sResourceCommon, - ResourceLink, - useActiveNamespace, + NamespaceBar, HorizontalNav, - useListPageFilter, - ListPageBody, - ListPageCreate, - ListPageFilter, - VirtualizedTable, - TableData, - RowProps, TableColumn, - NamespaceBar, - Timestamp, + K8sResourceCommon, + useActiveNamespace, useActivePerspective, + ListPageCreate, + ListPageBody } from '@openshift-console/dynamic-plugin-sdk'; -import './kuadrant.css'; -import getModelFromResource from '../utils/getModelFromResource'; +import { Title } from '@patternfly/react-core'; +import { Alert, AlertGroup } from '@patternfly/react-core'; +import ResourceList from './ResourceList'; +import './kuadrant.css'; interface Resource { name: string; @@ -49,222 +27,90 @@ interface Resource { }; } -interface ExtendedK8sResourceCommon extends K8sResourceCommon { - status?: { - addresses?: { value: string }[]; - }; -} - -const statusConditionsAsString = (obj: any) => { - if (!obj.status || !obj.status.conditions) { - return ''; - } - return obj.status.conditions - .map(condition => `${condition.type}=${condition.status}`) - .join(','); -}; - -const resources: Resource[] = [ +export const resources: Resource[] = [ { name: 'AuthPolicies', gvk: { group: 'kuadrant.io', version: 'v1beta2', kind: 'AuthPolicy' } }, { name: 'DNSPolicies', gvk: { group: 'kuadrant.io', version: 'v1alpha1', kind: 'DNSPolicy' } }, { name: 'RateLimitPolicies', gvk: { group: 'kuadrant.io', version: 'v1beta2', kind: 'RateLimitPolicy' } }, { name: 'TLSPolicies', gvk: { group: 'kuadrant.io', version: 'v1alpha1', kind: 'TLSPolicy' } }, ]; -type AllPoliciesTableProps = { - data: K8sResourceCommon[]; - unfilteredData: K8sResourceCommon[]; - loaded: boolean; - loadError: any; -}; - -type DropdownWithKebabProps = { - obj: K8sResourceCommon; -}; - -type PoliciesTableProps = { - data: K8sResourceCommon[]; - unfilteredData: K8sResourceCommon[]; - loaded: boolean; - loadError: any; - resource: Resource; -}; - -const AllPoliciesTable: React.FC = ({ data, unfilteredData, loaded, loadError }) => { +export const AllPoliciesListPage: React.FC<{ + activeNamespace: string; + columns?: TableColumn[]; + showAlertGroup?: boolean; + paginationLimit?: number; +}> = ({ activeNamespace, columns, showAlertGroup = true, paginationLimit }) => { const { t } = useTranslation(); - const columns: TableColumn[] = [ - { - title: t('plugin__console-plugin-template~Name'), - id: 'name', - sort: 'metadata.name', - transforms: [sortable], - }, - { - title: t('plugin__console-plugin-template~Type'), - id: 'type', - sort: 'kind', - transforms: [sortable], - }, - { - title: t('plugin__console-plugin-template~Namespace'), - id: 'namespace', - sort: 'metadata.namespace', - transforms: [sortable], - }, - { - title: t('plugin__console-plugin-template~Status'), - id: 'Status', - }, - { - title: t('plugin__console-plugin-template~Created'), - id: 'Created', - sort: 'metadata.creationTimestamp', - transforms: [sortable], - }, - { - title: '', // No title for the kebab menu column - id: 'dropdown-with-kebab', - props: { className: 'pf-v5-c-table__action' }, - }, - ]; - - const AllPolicyRow: React.FC> = ({ obj, activeColumnIDs }) => { - const [group, version] = obj.apiVersion.includes('/') ? obj.apiVersion.split('/') : ['', obj.apiVersion]; - return ( - <> - - - - {obj.kind} - - - - {statusConditionsAsString(obj)} - - - - - - - - ); - }; - return ( - - data={data} - unfilteredData={unfilteredData} - loaded={loaded} - loadError={loadError} - columns={columns} - Row={AllPolicyRow} - /> + <> + + {showAlertGroup && ( + + + ... + + + )} + r.gvk)} + namespace={activeNamespace} + columns={columns} + paginationLimit={paginationLimit} + /> + + ); }; -const DropdownWithKebab: React.FC = ({ obj }) => { - const { t } = useTranslation('plugin__console-plugin-template'); - const [isOpen, setIsOpen] = React.useState(false); - const [isDeleteModalOpen, setIsDeleteModalOpen] = React.useState(false); - - const onToggleClick = () => { - setIsOpen(!isOpen); - }; - - const onDeleteConfirm = async () => { - try { - const model = getModelFromResource(obj); - await k8sDelete({ model, resource: obj }); - console.log('Successfully deleted', obj.metadata.name); - } catch (error) { - console.error('Failed to delete', obj.metadata.name, error); - } finally { - setIsDeleteModalOpen(false); - } - }; - - const onDeleteClick = () => { - setIsDeleteModalOpen(true); - }; - - const onSelect = ( - _event: React.MouseEvent | undefined, - value: string | number | undefined - ) => { - setIsOpen(false); - if (value === 'delete') { - onDeleteClick(); - } - }; +const PoliciesListPage: React.FC<{ resource: Resource; activeNamespace: string }> = ({ resource, activeNamespace }) => { + const { t } = useTranslation(); return ( <> - setIsOpen(isOpen)} - toggle={(toggleRef: React.Ref) => ( - - - - )} - shouldFocusToggleOnSelect - > - - - {t('Edit')} - - - {t('Delete')} - - - - - setIsDeleteModalOpen(false)} - aria-labelledby="delete-modal-title" - aria-describedby="delete-modal-body" - variant="medium" - > - - - {t("Are you sure you want to delete the policy")}: {obj.metadata.name}? - - - - - - + + + + {/* Add any informational content here */} + ... + + +
+ + + {t(`plugin__console-plugin-template~Create ${resource.gvk.kind}`)} + +
+
); }; -const PoliciesTable: React.FC = ({ data, unfilteredData, loaded, loadError, resource }) => { - const { t } = useTranslation(); +const KuadrantPoliciesPage: React.FC = () => { + const { t } = useTranslation('plugin__console-plugin-template'); + const { ns } = useParams<{ ns: string }>(); + const [activeNamespace, setActiveNamespace] = useActiveNamespace(); + const [activePerspective] = useActivePerspective(); + + React.useEffect(() => { + if (ns && ns !== activeNamespace) { + setActiveNamespace(ns); + } + }, [ns, activeNamespace, setActiveNamespace]); - const columns: TableColumn[] = [ + const defaultColumns: TableColumn[] = [ { title: t('plugin__console-plugin-template~Name'), id: 'name', sort: 'metadata.name', transforms: [sortable], }, + { + title: t('plugin__console-plugin-template~Type'), + id: 'type', + sort: 'kind', + transforms: [sortable], + }, { title: t('plugin__console-plugin-template~Namespace'), id: 'namespace', @@ -282,166 +128,67 @@ const PoliciesTable: React.FC = ({ data, unfilteredData, loa transforms: [sortable], }, { - title: '', // No title for the kebab menu column - id: 'dropdown-with-kebab', + title: '', // No title for the kebab menu column + id: 'kebab', props: { className: 'pf-v5-c-table__action' }, }, ]; - const PolicyRow: React.FC> = ({ obj, activeColumnIDs }) => { - return ( - <> - - - - - - - {statusConditionsAsString(obj)} - - - - - - - - ); - }; - - return ( - - data={data} - unfilteredData={unfilteredData} - loaded={loaded} - loadError={loadError} - columns={columns} - Row={PolicyRow} - /> + const All: React.FC = () => ( + ); -}; - -const AllPoliciesListPage: React.FC<{ activeNamespace: string }> = ({ activeNamespace }) => { - const watchedResources = resources.map((resource) => { - const { group, version, kind } = resource.gvk; - return useK8sWatchResource({ - groupVersionKind: { group, version, kind }, - namespace: activeNamespace === '#ALL_NS#' ? undefined : activeNamespace, - isList: true, - }); - }); - - const policies = watchedResources.flatMap(([res_policies]) => res_policies || []); - const loaded = watchedResources.every(([_, res_loaded]) => res_loaded); - const loadError = watchedResources.some(([_, __, res_loadError]) => res_loadError); - const [data, filteredData, onFilterChange] = useListPageFilter(policies); - - return ( - <> - - - - ... - - - - - - + const Auth: React.FC = () => ( + ); -}; - -const PoliciesListPage: React.FC<{ resource: Resource; activeNamespace: string }> = ({ resource, activeNamespace }) => { - const { group, version, kind } = resource.gvk; - const [policies, loaded, loadError] = useK8sWatchResource({ - groupVersionKind: { group, version, kind }, - namespace: activeNamespace === '#ALL_NS#' ? undefined : activeNamespace, - isList: true, - }); - const { t } = useTranslation(); - - const [data, filteredData, onFilterChange] = useListPageFilter(policies); - return ( - <> - - - - ... - - -
- - - {t(`plugin__console-plugin-template~Create ${resource.gvk.kind}`)} - -
- -
- + const RateLimit: React.FC = () => ( + ); -}; - -const KuadrantPoliciesPage: React.FC = () => { - const { t } = useTranslation('plugin__console-plugin-template'); - const { ns } = useParams<{ ns: string }>(); - const [activeNamespace, setActiveNamespace] = useActiveNamespace(); - const [activePerspective] = useActivePerspective(); - - React.useEffect(() => { - if (ns && ns !== activeNamespace) { - setActiveNamespace(ns); - } - }, [ns, activeNamespace, setActiveNamespace]); - - const All: React.FC = () => ; - const Auth: React.FC = () => ; - const RateLimit: React.FC = () => ; let pages = [ { href: '', name: t('All Policies'), - component: All - } + component: All, + }, ]; if (activePerspective === 'admin') { - const DNS: React.FC = () => ; - const TLS: React.FC = () => ; + const DNS: React.FC = () => ( + + ); + const TLS: React.FC = () => ( + + ); pages = [ ...pages, { href: 'dns', name: t('DNS'), - component: DNS + component: DNS, }, { href: 'tls', name: t('TLS'), - component: TLS - } + component: TLS, + }, ]; } + pages = [ ...pages, { href: 'auth', name: t('Auth'), - component: Auth + component: Auth, }, { href: 'ratelimit', name: t('RateLimit'), - component: RateLimit - } + component: RateLimit, + }, ]; return ( diff --git a/src/components/ResourceList.tsx b/src/components/ResourceList.tsx new file mode 100644 index 0000000..03667f2 --- /dev/null +++ b/src/components/ResourceList.tsx @@ -0,0 +1,239 @@ +import * as React from 'react'; +import { useTranslation } from 'react-i18next'; +import { sortable } from '@patternfly/react-table'; +import { Alert, AlertGroup, Pagination } from '@patternfly/react-core'; +import { + K8sResourceCommon, + ResourceLink, + useK8sWatchResources, + VirtualizedTable, + useListPageFilter, + Timestamp, + TableData, + RowProps, + TableColumn, + WatchK8sResource, + ListPageBody, + ListPageFilter, +} from '@openshift-console/dynamic-plugin-sdk'; +import DropdownWithKebab from './DropdownWithKebab'; + +const statusConditionsAsString = (obj: any) => { + if (!obj.status || !obj.status.conditions) { + return ''; + } + return obj.status.conditions + .map((condition: any) => `${condition.type}=${condition.status}`) + .join(','); +}; + +type ResourceListProps = { + resources: Array<{ + group: string; + version: string; + kind: string; + }>; + namespace?: string; + paginationLimit?: number; + columns?: TableColumn[]; +}; + +const ResourceList: React.FC = ({ + resources, + namespace = '#ALL_NS#', + paginationLimit = 10, + columns, +}) => { + const { t } = useTranslation(); + + const resourceDescriptors: { [key: string]: WatchK8sResource } = resources.reduce( + (acc, resource, index) => { + const key = `${resource.group}-${resource.version}-${resource.kind}-${index}`; + acc[key] = { + groupVersionKind: { + group: resource.group, + version: resource.version, + kind: resource.kind, + }, + namespace: namespace === '#ALL_NS#' ? undefined : namespace, + isList: true, + }; + return acc; + }, + {} as { [key: string]: WatchK8sResource }, + ); + + const watchedResources = useK8sWatchResources<{ [key: string]: K8sResourceCommon[] }>( + resourceDescriptors, + ); + + const allData = Object.values(watchedResources).flatMap((res) => + res.loaded && !res.loadError ? (res.data as K8sResourceCommon[]) : [], + ); + + const allLoaded = Object.values(watchedResources).every((res) => res.loaded); + + const loadErrors = Object.values(watchedResources) + .filter((res) => res.loadError) + .map((res) => res.loadError); + + const combinedLoadError = + loadErrors.length > 0 ? new Error(loadErrors.map((err) => err.message).join('; ')) : null; + + const [data, filteredData, onFilterChange] = useListPageFilter(allData); + + const defaultColumns: TableColumn[] = [{ + title: t('plugin__console-plugin-template~Name'), + id: 'name', + sort: 'metadata.name', + transforms: [sortable], + }, { + title: t('plugin__console-plugin-template~Type'), + id: 'type', + sort: 'kind', + transforms: [sortable], + }, { + title: t('plugin__console-plugin-template~Namespace'), + id: 'namespace', + sort: 'metadata.namespace', + transforms: [sortable], + }, { + title: t('plugin__console-plugin-template~Status'), + id: 'Status', + }, { + title: t('plugin__console-plugin-template~Created'), + id: 'Created', + sort: 'metadata.creationTimestamp', + transforms: [sortable], + }, { + title: '', // No title for the kebab column + id: 'kebab', + props: { className: 'pf-v5-c-table__action' }, + }]; + + const usedColumns = columns || defaultColumns; + + const [currentPage, setCurrentPage] = React.useState(1); + const [perPage, setPerPage] = React.useState(paginationLimit); + + const startIndex = (currentPage - 1) * perPage; + const endIndex = startIndex + perPage; + const paginatedData = filteredData.slice(startIndex, endIndex); + + const onSetPage = ( + _event: React.MouseEvent | React.KeyboardEvent | MouseEvent, + pageNumber: number, + ) => { + setCurrentPage(pageNumber); + }; + + const onPerPageSelect = ( + _event: React.MouseEvent | React.KeyboardEvent | MouseEvent, + perPageNumber: number, + ) => { + setPerPage(perPageNumber); + setCurrentPage(1); + }; + + const ResourceRow: React.FC> = ({ obj, activeColumnIDs }) => { + const { apiVersion, kind } = obj; + const [group, version] = apiVersion.includes('/') ? apiVersion.split('/') : ['', apiVersion]; + + return ( + <> + {usedColumns.map((column) => { + switch (column.id) { + case 'name': + return ( + + + + ); + case 'type': + return ( + + {kind} + + ); + case 'namespace': + return ( + + + + ); + case 'Status': + return ( + + {statusConditionsAsString(obj)} + + ); + case 'Created': + return ( + + + + ); + case 'kebab': + return ( + + + + ); + default: + return null; + } + })} + + ); + }; + + return ( + <> + {combinedLoadError && ( + + + {combinedLoadError.message} + + + )} + + + + data={paginatedData} + unfilteredData={data} + loaded={allLoaded} + loadError={combinedLoadError} + columns={usedColumns} + Row={ResourceRow} + /> + + + + ); +}; + +export default ResourceList; diff --git a/src/components/kuadrant.css b/src/components/kuadrant.css index 2049847..1dbd323 100644 --- a/src/components/kuadrant.css +++ b/src/components/kuadrant.css @@ -45,4 +45,66 @@ max-height: 450px; overflow-y: auto; padding: 1rem; -} \ No newline at end of file +} + +.kuadrant-dashboard-learning { + color: #1F0066; +} + +@media (prefers-color-scheme: light) { + .kuadrant-dashboard-learning { + color: #1F0066; + } +} + +@media (prefers-color-scheme: dark) { + .kuadrant-dashboard-learning { + color: #9c90b9; + } +} + + + + +.kuadrant-dashboard-feature-highlights { + color: #0066CC; +} + +.kuadrant-dashboard-enhance { + color: #D38940; +} + +.kuadrant-dashboard-learning, .kuadrant-dashboard-feature-highlights, .kuadrant-dashboard-enhance { + margin-bottom: 0.5em; +} + +.kuadrant-dashboard-resource-link { + text-decoration: none; + color: black; + cursor: pointer; + font-size: 0.9rem; +} + +@media (prefers-color-scheme: light) { + .kuadrant-dashboard-resource-link{ + color: black; + } +} + +@media (prefers-color-scheme: dark) { + .kuadrant-dashboard-resource-link { + color: white; + } +} + +.kuadrant-dashboard-resource-link:hover { + text-decoration: underline; + color: #0066cc; +} + +.kuadrant-reading-time { + font-size: 0.8rem; + color: grey; + margin-left: 1rem; + margin-right: 1rem +} diff --git a/src/constants/links.ts b/src/constants/links.ts new file mode 100644 index 0000000..28e461b --- /dev/null +++ b/src/constants/links.ts @@ -0,0 +1,20 @@ +// Internal Links +export const INTERNAL_LINKS = { + createPolicies: "/kuadrant/all-namespaces/policies", + apiDesigner: 'https://www.apicur.io/studio/', + addNewGateway: (namespace: string) => + `/k8s/ns/${namespace === '#ALL_NS#' ? 'default' : namespace}/gateway.networking.k8s.io~v1~Gateway/~new`, + observabilitySetup: 'https://docs.kuadrant.io/latest/kuadrant-operator/doc/observability/examples/', + certManagerOperator: (namespace: string) => + `/operatorhub/ns/${namespace === '#ALL_NS#' ? 'default' : namespace}?keyword=cert-manager&details-item=openshift-cert-manager-operator-redhat-operators-openshift-marketplace` +}; + +// External Links +export const EXTERNAL_LINKS = { + // TODO: Update these when available for real + documentation: "https://docs.kuadrant.io", + releaseNotes: "https://github.com/Kuadrant/kuadrant-operator/releases", + quickStarts: "https://docs.kuadrant.io/latest/kuadrant-operator/doc/user-guides/secure-protect-connect/", + highlights: "https://kuadrant.io/blog/", + blog: "https://kuadrant.io/blog/", +}; diff --git a/yarn.lock b/yarn.lock index e1d1572..f041e0a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1974,11 +1974,16 @@ resolved "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-4.93.7.tgz" integrity sha512-3kr35dgba7Qz5CSzmfH0rIjSvBC5xkmiknf3SvVUVxaiVA7KRowID8viYHeZlf3v/Oa3sEewaH830Q0t+nWsZQ== -"@patternfly/react-icons@^5.1.1", "@patternfly/react-icons@^5.3.2": +"@patternfly/react-icons@^5.3.2": version "5.3.2" resolved "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-5.3.2.tgz" integrity sha512-GEygYbl0H4zD8nZuTQy2dayKIrV2bMMeWKSOEZ16Y3EYNgYVUOUnN+J0naAEuEGH39Xb1DE9n+XUbE1PC4CxPA== +"@patternfly/react-icons@^5.4.0": + version "5.4.0" + resolved "https://registry.yarnpkg.com/@patternfly/react-icons/-/react-icons-5.4.0.tgz#bc6aebfdce79a2fb1b50ffdc7fd7d171c719c25b" + integrity sha512-2M3qN/naultvRHeG2laJMmoIroFCGAyfwTVrnCjSkG6/KnRoXV0+dqd+Xrh7xzpzvIJB1klvifC0oX42cEkDrA== + "@patternfly/react-styles@^4.92.8": version "4.92.8" resolved "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-4.92.8.tgz"