From f97083f42f5753746f407591b31e6ac7835b6b8f Mon Sep 17 00:00:00 2001 From: Phillip Bailey Date: Thu, 18 May 2023 07:39:07 -0400 Subject: [PATCH] Topology plugin --- .deepsource.toml | 13 + .eslintignore | 3 + .eslintrc.json | 70 + .gitignore | 23 + .prettierrc | 8 + Dockerfile | 13 + INTERNATIONALIZATION.md | 98 + OWNERS | 21 + console-extensions.json | 259 + i18next-parser.config.mjs | 10 + images/logos/3scale.svg | 1 + images/logos/aerogear.svg | 48 + images/logos/amq.svg | 1 + images/logos/angularjs.svg | 18 + images/logos/ansible.svg | 12 + images/logos/apache.svg | 148 + images/logos/beaker.svg | 35 + images/logos/build-icon.svg | 9 + images/logos/camel.svg | 1 + images/logos/capedwarf.svg | 133 + images/logos/cassandra.svg | 182 + images/logos/catalog-icon.svg | 1 + images/logos/clojure.svg | 33 + images/logos/codeigniter.svg | 22 + images/logos/cordova.png | Bin 0 -> 9713 bytes images/logos/datagrid.svg | 1 + images/logos/datavirt.svg | 1 + images/logos/debian.svg | 39 + images/logos/decisionserver.svg | 1 + images/logos/django.svg | 37 + images/logos/dotnet.svg | 7 + images/logos/drupal.svg | 47 + images/logos/eap.svg | 1 + images/logos/elastic.svg | 31 + images/logos/erlang.svg | 30 + images/logos/fedora.svg | 23 + images/logos/freebsd.svg | 1 + images/logos/git.svg | 22 + images/logos/github.svg | 15 + images/logos/gitlab.svg | 19 + images/logos/glassfish.svg | 60 + images/logos/go-gopher.svg | 154 + images/logos/golang.svg | 60 + images/logos/grails.svg | 24 + images/logos/hadoop.svg | 175 + images/logos/haproxy.svg | 111 + images/logos/helm.svg | 2 + images/logos/infinispan.svg | 59 + images/logos/jboss.svg | 89 + images/logos/jenkins.svg | 163 + images/logos/jetty.svg | 72 + images/logos/joomla.svg | 25 + images/logos/jruby.svg | 24 + images/logos/js.svg | 14 + images/logos/knative.svg | 1 + images/logos/kubevirt.svg | 96 + images/logos/laravel.svg | 28 + images/logos/load-balancer.svg | 14 + images/logos/mariadb.svg | 37 + images/logos/mediawiki.svg | 111 + images/logos/memcached.svg | 58 + images/logos/mongodb.svg | 42 + images/logos/mssql.svg | 115 + images/logos/mysql-database.svg | 1 + images/logos/nginx.svg | 37 + images/logos/nodejs.svg | 140 + images/logos/openjdk.svg | 44 + images/logos/openliberty.svg | 1 + images/logos/openshift.svg | 24 + images/logos/openstack.svg | 14 + images/logos/operator.svg | 1 + images/logos/other-linux.svg | 686 + images/logos/other-unknown.svg | 16 + images/logos/perl.svg | 57 + images/logos/phalcon.svg | 43 + images/logos/php.svg | 1 + images/logos/play.svg | 16 + images/logos/postgresql.svg | 101 + images/logos/processserver.svg | 1 + images/logos/python.svg | 28 + images/logos/quarkus.svg | 1 + images/logos/rabbitmq.svg | 14 + images/logos/rails.svg | 31 + images/logos/redhat.svg | 1 + images/logos/redis.svg | 36 + images/logos/rh-integration.svg | 1 + images/logos/rh-spring-boot.svg | 1 + images/logos/rh-tomcat.svg | 1 + images/logos/ruby.svg | 177 + images/logos/scala.svg | 46 + images/logos/serverlessfx.svg | 42 + images/logos/shadowman.svg | 44 + images/logos/spring-boot.svg | 1 + images/logos/spring.svg | 20 + images/logos/sso.svg | 1 + images/logos/stackoverflow.svg | 13 + images/logos/suse.svg | 31 + images/logos/symfony.svg | 1 + images/logos/tomcat.svg | 71 + images/logos/ubuntu.svg | 20 + images/logos/vertx.svg | 1 + images/logos/wildfly.svg | 337 + images/logos/windows.svg | 11 + images/logos/wordpress.svg | 24 + images/logos/xamarin.svg | 6 + images/logos/zend.svg | 15 + images/restricted-sign.svg | 24 + integration-tests/.eslintrc | 6 + integration-tests/OWNERS | 7 + integration-tests/cypress.json | 33 + .../features/e2e/topology-ci.feature | 60 + .../export-of-applicaton.feature | 77 + .../action-on-hpa-admin-perspective.feature | 81 + .../actions-on-hpa.feature | 129 + .../hpa-topology-sidebar.feature | 22 + .../topology/pipelines-on-topology.feature | 115 + .../topology/quick-search-topology.feature | 125 + .../topology/resource-quota-warning.feature | 49 + .../topology/resources-groupings.feature | 142 + ...pology-add-operator-backed-service.feature | 54 + .../topology-application-grouping.feature | 54 + .../topology-chart-area-visual.feature | 506 + .../topology-connecting-workloads.feature | 29 + .../topology/topology-display-filter.feature | 58 + .../topology-editing-app-node.feature | 153 + .../topology-filter-bar-display.feature | 69 + .../topology-in-context-add-options.feature | 33 + .../topology/topology-layout-save.feature | 49 + .../topology/topology-list-view.feature | 42 + .../topology/topology-toolbar-filter.feature | 20 + .../topology-workload-sidebar.feature | 87 + .../topoplogy-delete-workload.feature | 27 + .../topology/workloads-on-topology.feature | 27 + integration-tests/package.json | 14 + integration-tests/plugins/index.js | 87 + integration-tests/reporter-config.json | 14 + integration-tests/support/commands/hooks.ts | 15 + integration-tests/support/commands/index.ts | 8 + .../support/page-objects/chart-area-po.ts | 48 + .../page-objects/export-applications-po.ts | 38 + .../support/page-objects/hpa-po.ts | 27 + .../page-objects/topology-add-options-po.ts | 3 + .../support/page-objects/topology-po.ts | 336 + .../export-application/export-applications.ts | 42 + .../support/pages/functions/add-secret.ts | 60 + .../pages/functions/chart-functions.ts | 127 + .../support/pages/topology/index.ts | 4 + .../pages/topology/topology-actions-page.ts | 230 + .../topology/topology-edit-deployment.ts | 126 + .../pages/topology/topology-helper-page.ts | 32 + .../support/pages/topology/topology-page.ts | 512 + .../pages/topology/topology-side-pane-page.ts | 174 + .../step-definitions/common/topology.ts | 221 + .../export-of-application.ts | 112 + .../topology/application-groupings.ts | 120 + .../step-definitions/topology/chart-view.ts | 491 + .../topology/create-workloads.ts | 30 + .../topology/delete-workload.ts | 18 + .../topology/edit-workload.ts | 264 + .../action-on-hpa.ts | 256 + .../step-definitions/topology/list-view.ts | 22 + .../topology/pipelines-on-topology.ts | 176 + .../topology/quick-search-topology.ts | 211 + .../topology/resource-quota-warning.ts | 90 + .../topology/topology-add-options.ts | 102 + .../topology/topology-filters.ts | 131 + .../topology/workload-sidebar.ts | 166 + .../support/test-data/bindable-resource1.yaml | 35 + .../test-data/hippo-postgres-cluster.yaml | 38 + .../test-data/postgres-operator-backed.yaml | 48 + .../support/test-data/redis-standalone.yaml | 23 + ...ervicebinding-resource-label-selector.yaml | 17 + .../resource-quota/resource-quota.yaml | 15 + integration-tests/tsconfig.json | 12 + jest.config.ts | 41 + locales/en/expNamespace.json | 3 + locales/en/plugin__topology-plugin.json | 338 + locales/en/public.json | 25 + oc-manifest.yaml | 111 + package.json | 163 + scripts/start-console.sh | 42 + src/__tests__/CloseButton/CloseButton.scss | 14 + src/__tests__/CloseButton/CloseButton.tsx | 37 + src/__tests__/DataModelProvider.spec.tsx | 47 + src/__tests__/Graph.spec.tsx | 79 + src/__tests__/TopologyPage.spec.tsx | 158 + src/__tests__/TopologyPageToolbar.spec.tsx | 119 + src/__tests__/TopologyShortcuts.spec.tsx | 117 + src/__tests__/TopologySideBar.spec.tsx | 36 + src/__tests__/graph-test-data.ts | 2631 ++++ src/__tests__/service-binding-test-data.ts | 301 + src/__tests__/topology-test-data.ts | 306 + src/__tests__/topology-utils.spec.ts | 53 + src/actions/TopologyActions.tsx | 39 + .../__tests__/contextMenuActions.spec.ts | 54 + src/actions/contextMenuActions.tsx | 61 + src/actions/edgeActions.ts | 116 + src/actions/index.ts | 5 + src/actions/modify-application.ts | 30 + src/actions/nodeActions.ts | 13 + src/actions/provider.ts | 45 + src/behavior/index.ts | 2 + src/behavior/useHover.ts | 95 + src/behavior/withCreateConnector.tsx | 336 + src/components/ActionMenu/ActionMenu.tsx | 117 + .../ActionMenuContent/ActionMenuContent.tsx | 102 + .../ActionMenuItem/ActionMenuItem.tsx | 46 + .../components/AccessReviewActionItem.tsx | 20 + .../ActionMenuItem/components/ActionItem.tsx | 74 + .../ActionMenuContent/utils/utils.ts | 105 + .../components/ActionMenuToggle.tsx | 98 + .../components/ActionsMenuDropdown.tsx | 84 + .../ApplicationGroupResource.tsx | 48 + .../TopologyApplicationList.tsx | 36 + .../TopologyApplicationResources.scss | 5 + .../TopologyApplicationResources.tsx | 40 + .../ApplicationGroupResource.spec.tsx | 77 + .../application-resource-link.tsx | 16 + src/components/application-panel/index.ts | 2 + .../useApplicationResourceTabSection.tsx | 25 + src/components/common/CopyToClipboard.tsx | 55 + .../common/DeploymentConfigDetailsList.tsx | 85 + .../DeploymentDetailsList.tsx | 64 + .../components/RuntimeClass.tsx | 25 + .../common/DetailsItem/DetailsItem.tsx | 109 + .../DetailsItem/components/EditButton.tsx | 28 + .../components/LinkifyExternal.tsx | 8 + .../DetailsItem/components/PropertyPath.tsx | 23 + src/components/common/ExternalLink.tsx | 32 + src/components/common/LabelList/LabelList.tsx | 49 + .../common/LabelList/components/Label.tsx | 36 + .../common/LabelsModal/BaseLabelsModal.tsx | 158 + .../LabelsModal/components/LabelsModal.tsx | 10 + .../LabelsModal/utils/SelectorInput.tsx | 190 + .../common/LabelsModal/utils/const.ts | 2 + src/components/common/ListPage.tsx | 0 .../common/LoadingInline/LoadingInline.tsx | 7 + .../common/LoadingInline/components/Box.tsx | 13 + .../common/LoadingInline/components/Data.tsx | 42 + .../LoadingInline/components/EmptyBox.tsx | 22 + .../LoadingInline/components/LoadError.tsx | 49 + .../LoadingInline/components/Loading.tsx | 19 + .../LoadingInline/components/MsgBox.tsx | 26 + .../ManagedByOperatorLink.tsx | 53 + .../ManagedByOperatorResourceLink.tsx | 48 + .../ManagedByOperatorLink/utils/utils.ts | 36 + src/components/common/OwnerReferences.tsx | 26 + .../common/PageHeading/PageHeading.tsx | 146 + .../PageHeading/components/BreadCrumbs.tsx | 34 + .../common/PageHeading/utils/types.ts | 35 + .../PodDisruptionBudgetField.tsx | 54 + .../components/AvailabilityRequirement.tsx | 29 + .../PodDisruptionBudgetField/utils/utils.ts | 13 + src/components/common/PodRing/PodRing.scss | 6 + src/components/common/PodRing/PodRing.tsx | 147 + .../utils/hooks/usePodScalingAccessStatus.ts | 45 + src/components/common/PodRingSet.tsx | 74 + src/components/common/PodTraffic.tsx | 56 + .../ResourceSummary/ResourceSummary.tsx | 146 + .../common/ResourceSummary/utils/utils.tsx | 30 + src/components/common/Selector.tsx | 28 + .../Selector/components/Requirement.tsx | 33 + src/components/common/StatusBox/StatusBox.tsx | 94 + .../StatusBox/components/AccessDenied.tsx | 35 + .../StatusBox/components/LoadingBox.tsx | 19 + src/components/common/radio/RadioGroup.tsx | 60 + src/components/common/radio/RadioInput.tsx | 44 + .../dev-console/CreateProjectButton.tsx | 28 + .../dev-console/CreateProjectListPage.tsx | 32 + .../dev-console/ProjectListPage.tsx | 37 + .../ResourceQuotaAlert/ResourceQuotaAlert.tsx | 133 + .../ResourceQuotaAlert/types/types.ts | 40 + .../dropdowns/ApplicationDropdown.tsx | 85 + .../dropdowns/ApplicationSelector.tsx | 138 + .../NamespaceBarApplicationSelector.tsx | 91 + .../__tests__/ApplicationSelector.spec.tsx | 112 + src/components/error/ErrorBoundary.tsx | 77 + src/components/error/ErrorBoundaryFC.tsx | 80 + .../error/ErrorBoundaryFallbackPage.tsx | 27 + src/components/error/ErrorDetailsBlock.tsx | 33 + src/components/error/ExpandCollapse.tsx | 35 + src/components/error/withFallback.tsx | 20 + .../export-app/ExportApplication.tsx | 49 + .../export-app/ExportApplicationModal.tsx | 261 + .../export-app/ExportViewLogButton.tsx | 83 + .../__tests__/ExportApplication.spec.tsx | 61 + .../__tests__/ExportApplicationModal.spec.tsx | 138 + .../__tests__/ExportViewLogButton.spec.tsx | 160 + .../export-app/__tests__/export-data.ts | 52 + src/components/export-app/types.ts | 10 + .../export-app/useExportAppFormToast.ts | 160 + .../formik-fields/BaseInputField.tsx | 62 + .../formik-fields/InputField/InputField.scss | 51 + .../formik-fields/InputField/InputField.tsx | 29 + src/components/formik-fields/utils/types.ts | 25 + src/components/graph-view/Topology.scss | 28 + src/components/graph-view/Topology.tsx | 398 + .../graph-view/TopologyControlBar.scss | 5 + .../graph-view/TopologyControlBar.tsx | 61 + .../graph-view/TopologyShortcuts.tsx | 57 + .../graph-view/components/ContextMenu.scss | 15 + .../graph-view/components/GraphComponent.scss | 27 + .../graph-view/components/GraphComponent.tsx | 51 + .../graph-view/components/NodeShadows.tsx | 20 + .../graph-view/components/RegroupHint.scss | 14 + .../graph-view/components/RegroupHint.tsx | 27 + .../components/Shortcut/Shortcut.scss | 16 + .../components/Shortcut/Shortcut.tsx | 88 + .../Shortcut/components/ShortcutCommand.tsx | 9 + .../graph-view/components/ShortcutTable.tsx | 9 + .../graph-view/components/componentFactory.ts | 90 + .../graph-view/components/componentUtils.ts | 409 + .../components/edges/AggregateEdge.scss | 5 + .../components/edges/AggregateEdge.tsx | 32 + .../graph-view/components/edges/BaseEdge.scss | 70 + .../graph-view/components/edges/BaseEdge.tsx | 80 + .../components/edges/ConnectsTo.scss | 14 + .../components/edges/ConnectsTo.tsx | 30 + .../components/edges/CreateConnector.tsx | 34 + .../components/edges/ServiceBinding.scss | 3 + .../components/edges/ServiceBinding.tsx | 86 + .../components/edges/TrafficConnector.scss | 4 + .../components/edges/TrafficConnector.tsx | 21 + .../graph-view/components/edges/index.ts | 6 + .../components/groups/Application.scss | 46 + .../components/groups/Application.tsx | 103 + .../groups/ApplicationGroupExpanded.tsx | 241 + .../components/groups/ApplicationNode.tsx | 44 + .../components/groups/GroupNode.scss | 51 + .../components/groups/GroupNode.tsx | 156 + .../components/groups/GroupNodeAnchor.ts | 25 + .../components/groups/ResourceKindsInfo.scss | 29 + .../components/groups/ResourceKindsInfo.tsx | 80 + .../graph-view/components/groups/index.ts | 4 + .../components/groups/utils/utils.ts | 12 + src/components/graph-view/components/index.ts | 7 + .../graph-view/components/nodeContextMenu.tsx | 28 + .../graph-view/components/nodes/BaseNode.scss | 38 + .../graph-view/components/nodes/BaseNode.tsx | 168 + .../components/nodes/BindableNode.tsx | 62 + .../components/nodes/PodSet/PodSet.tsx | 74 + .../components/nodes/PodSet/utils/utils.ts | 36 + .../components/nodes/PodStatus/PodStatus.scss | 14 + .../components/nodes/PodStatus/PodStatus.tsx | 175 + .../nodes/PodStatus/hooks/useForceUpdate.ts | 13 + .../components/nodes/PodStatus/utils/utils.ts | 3 + .../components/nodes/WorkloadNode.scss | 13 + .../components/nodes/WorkloadNode.tsx | 258 + .../nodes/decorators/BuildDecorator.tsx | 48 + .../nodes/decorators/BuildDecoratorBubble.tsx | 28 + .../nodes/decorators/Decorator.scss | 21 + .../components/nodes/decorators/Decorator.tsx | 59 + .../nodes/decorators/EditDecorator.tsx | 49 + .../decorators/MonitoringAlertsDecorator.tsx | 65 + .../nodes/decorators/UrlDecorator.tsx | 39 + .../decorators/__tests__/Decorator.spec.tsx | 18 + .../__tests__/URLDecorator.spec.tsx | 136 + .../nodes/decorators/defaultDecorators.tsx | 24 + .../decorators/defaultDecoratorsPlugin.ts | 50 + .../nodes/decorators/getDefaultDecorators.ts | 13 + .../nodes/decorators/getNodeDecorators.tsx | 82 + .../components/nodes/decorators/index.ts | 6 + .../graph-view/components/nodes/index.ts | 4 + .../graph-view/components/nodes/nodeUtils.ts | 12 + .../components/CheIcon.tsx | 20 + .../route-decorator-icon.tsx | 57 + .../router-decorator-icon/types/types.ts | 7 + .../router-decorator-icon/utils/utils.tsx | 30 + .../components/withTopologyContextMenu.tsx | 73 + src/components/graph-view/index.ts | 2 + .../graph-view/layouts/TopologyColaLayout.ts | 28 + .../graph-view/layouts/layoutFactory.ts | 14 + .../helm/HelmReleaseListViewNode.tsx | 40 + .../knative/KnativeRevisionListViewNode.tsx | 93 + .../knative/KnativeServiceListViewNode.tsx | 44 + .../knative/NoStatusListViewNode.tsx | 34 + .../knative/SinkURIListViewNode.tsx | 29 + .../knative/icons/EventSourceIcon.tsx | 9 + src/components/knative/icons/icon-utils.tsx | 6 + .../list-view/ListElementWrapper.tsx | 74 + .../list-view/TopologyListView.scss | 229 + src/components/list-view/TopologyListView.tsx | 348 + .../list-view/TopologyListViewAppGroup.tsx | 107 + .../list-view/TopologyListViewKindGroup.tsx | 84 + .../list-view/TopologyListViewNode.tsx | 172 + .../TopologyListViewUnassignedGroup.tsx | 81 + .../list-view/cells/AlertsCell.scss | 19 + src/components/list-view/cells/AlertsCell.tsx | 139 + src/components/list-view/cells/CpuCell.tsx | 53 + .../list-view/cells/GroupResourcesCell.tsx | 60 + .../list-view/cells/MemoryCell.scss | 49 + src/components/list-view/cells/MemoryCell.tsx | 52 + .../list-view/cells/MetricsCell.scss | 49 + .../list-view/cells/MetricsTooltip.tsx | 73 + .../list-view/cells/StatusCell.scss | 22 + src/components/list-view/cells/StatusCell.tsx | 90 + .../cells/TypedResourceBadgeCell.tsx | 66 + src/components/list-view/cells/index.ts | 6 + src/components/list-view/index.ts | 5 + src/components/list-view/list-view-utils.tsx | 30 + .../list-view/listViewComponentFactory.ts | 44 + src/components/metal3/NodeLink.tsx | 25 + .../ConfigureUpdateStrategyModal.tsx | 83 + .../components/ConfigureUpdateStrategy.tsx | 145 + .../utils/utils.ts | 12 + .../modals/EditApplicationModal.tsx | 107 + src/components/modals/ErrorModal.tsx | 38 + src/components/modals/MoveConnectionModal.tsx | 180 + src/components/modals/ResourceLimitsModal.tsx | 66 + .../ServiceBindingModal.tsx | 110 + .../BindableServices/BindableServices.tsx | 102 + .../BindableServices/utils/utils.ts | 69 + .../components/CreateServiceBindingForm.tsx | 65 + .../modals/ServiceBindingModal/index.ts | 4 + .../modals/ServiceBindingModal/utils/utils.ts | 44 + src/components/modals/index.ts | 4 + src/components/page/DroppableTopology.tsx | 19 + .../page/DroppableTopology/utils/utils.ts | 7 + .../page/DroppableTopologyComponent.tsx | 38 + src/components/page/LimitExceededState.tsx | 42 + src/components/page/PageContents.tsx | 40 + src/components/page/TopologyDataRenderer.tsx | 44 + src/components/page/TopologyEmptyState.tsx | 57 + src/components/page/TopologyPage.tsx | 122 + src/components/page/TopologyPageToolbar.tsx | 83 + .../useAddToProjectAccess.ts | 69 + .../useAddToProjectAccess/utils/utils.ts | 13 + .../hooks/useIsMobile/useIsMobile.ts | 35 + .../hooks/useIsMobile/utils/const.ts | 2 + src/components/page/TopologyView.scss | 123 + src/components/page/TopologyView.tsx | 442 + .../quick-search/TopologyQuickSearch.tsx | 120 + .../TopologyQuickSearchButton.scss | 3 + .../TopologyQuickSearchButton.tsx | 34 + .../topology-quick-search-utils.tsx | 47 + .../side-bar/DefaultResourceSideBar.tsx | 18 + .../side-bar/SelectedEntityDetails.tsx | 51 + src/components/side-bar/TopologyEdgePanel.tsx | 66 + .../side-bar/TopologyEdgeResourcesPanel.tsx | 102 + .../side-bar/TopologyGroupResourceItem.tsx | 39 + .../side-bar/TopologyGroupResourceList.tsx | 34 + .../side-bar/TopologyGroupResourcesPanel.tsx | 51 + src/components/side-bar/TopologySideBar.tsx | 53 + .../side-bar/TopologySideBarContent.tsx | 17 + .../side-bar/TopologySideBarTabSection.scss | 7 + .../side-bar/TopologySideBarTabSection.tsx | 9 + .../TopologyEdgeResourcesPanel.spec.tsx | 81 + .../topology-edge-resources-panel-data.ts | 1121 ++ .../side-bar/components/SideBarAlerts.tsx | 70 + .../side-bar/components/SideBarBody.tsx | 45 + .../side-bar/components/SideBarHeading.scss | 3 + .../side-bar/components/SideBarHeading.tsx | 30 + .../providers/SideBarTabHookResolver.tsx | 122 + .../side-bar/providers/SideBarTabLoader.tsx | 34 + .../providers/useDetailsResourceLink.tsx | 22 + .../side-bar/providers/useDetailsTab.tsx | 18 + .../providers/useDetailsTabSection.tsx | 27 + src/components/svg/SvgBoxedText.tsx | 123 + src/components/svg/SvgCircledIcon.tsx | 74 + src/components/svg/SvgDropShadowFilter.tsx | 70 + src/components/svg/SvgResourceIcon.scss | 24 + src/components/svg/SvgResourceIcon.tsx | 78 + .../svg/__tests__/SvgBoxedText.spec.tsx | 56 + .../svg/__tests__/SvgResourceIcon.spec.tsx | 33 + src/components/svg/index.ts | 4 + src/components/utils/PromiseComponent.tsx | 47 + .../__tests__/CheckResourceQuota.spec.tsx | 38 + src/components/utils/__tests__/mockData.ts | 155 + src/components/utils/checkResourceQuota.ts | 52 + .../utils/subscribeOverviewAlerts.ts | 34 + .../utils/subscribeOverviewMetrics.ts | 50 + .../visual-connector/edge-resource-link.tsx | 9 + src/components/visual-connector/index.ts | 2 + .../visual-connector/resource-tab-section.tsx | 17 + .../workload/BuildOverview/BuildOverview.tsx | 31 + .../BuildOverview/components/BuildLogLink.tsx | 31 + .../components/BuildNumberLink.tsx | 21 + .../components/BuildOverviewItem.tsx | 55 + .../components/BuildOverviewList.tsx | 82 + .../components/BuildPipelineLogLink.tsx | 25 + .../BuildOverview/components/BuildStatus.tsx | 21 + .../BuildOverview/components/LogSnippet.tsx | 17 + .../BuildOverview/components/StatusTitle.tsx | 65 + .../workload/BuildOverview/utils/types.ts | 17 + .../workload/BuildOverview/utils/utils.ts | 26 + .../workload/CronJobSideBarDetails.tsx | 53 + .../workload/DaemonSetDetailsList.tsx | 24 + .../workload/DaemonSetSideBarDetails.tsx | 43 + .../DeploymentConfigSideBarDetails.tsx | 62 + .../workload/DeploymentSideBarDetails.tsx | 62 + .../workload/JobOverview/JobsOverview.tsx | 49 + .../components/JobOverviewItem.tsx | 48 + .../components/JobOverviewList.tsx | 19 + .../components/SidebarSectionHeading.tsx | 22 + src/components/workload/JobSideBarDetails.tsx | 56 + .../JobsTabSection/JobsTabSection.tsx | 23 + .../hooks/useJobsForCronJobWatcher.ts | 48 + .../hooks/useJobsSideBarTabSection.tsx | 19 + .../workload/NetworkingOverview.tsx | 117 + .../PodDetailsList/PodDetailsList.tsx | 63 + .../PodDetailsList/components/PodStatus.tsx | 103 + .../components/PodStatusPopover.tsx | 28 + .../workload/PodDetailsList/utils/utils.ts | 11 + .../workload/PodResourceSummary.tsx | 20 + src/components/workload/PodSideBarDetails.tsx | 41 + src/components/workload/ResolveAdapter.ts | 31 + .../workload/StatefulSetSideBarDetails.tsx | 34 + .../WorkloadPausedAlert.tsx | 32 + src/components/workload/build-tab-section.tsx | 71 + src/components/workload/index.ts | 15 + .../workload/network-tab-section.tsx | 47 + src/components/workload/pods-tab-section.tsx | 74 + src/components/workload/resource-alert.tsx | 141 + src/components/workload/utils.ts | 162 + .../workload/workload-resource-link.tsx | 27 + src/const.ts | 90 + src/data-transforms/DataModelExtension.tsx | 84 + src/data-transforms/DataModelProvider.tsx | 100 + src/data-transforms/ModelContext.ts | 215 + src/data-transforms/TopologyDataRetriever.tsx | 64 + .../data-transformer.spec.ts.snap | 2504 ++++ .../__tests__/data-transformer.spec.ts | 245 + .../__tests__/transform-utils.spec.ts | 49 + .../__tests__/updateModelFromFilters.spec.ts | 118 + .../__tests__/updateTopologyDataModel.spec.ts | 120 + src/data-transforms/data-transformer.ts | 146 + src/data-transforms/transform-utils.ts | 382 + src/data-transforms/updateModelFromFilters.ts | 148 + .../updateTopologyDataModel.ts | 63 + src/data-transforms/useMonitoringAlerts.tsx | 40 + src/data-transforms/useRoutesURL.ts | 22 + src/elements/BaseNode.ts | 408 + src/elements/OdcBaseEdge.ts | 43 + src/elements/OdcBaseNode.ts | 49 + src/elements/index.ts | 3 + src/elements/odcElementFactory.ts | 18 + src/extensions/index.ts | 1 + src/extensions/topology.ts | 104 + src/filters/FilterDropdown.scss | 38 + src/filters/FilterDropdown.tsx | 141 + src/filters/FilterProvider.tsx | 84 + src/filters/KindFilterDropdown.scss | 20 + src/filters/KindFilterDropdown.tsx | 112 + src/filters/NameLabelFilterDropdown.tsx | 92 + src/filters/TopologyFilterBar.scss | 19 + src/filters/TopologyFilterBar.tsx | 238 + src/filters/__tests__/FilterDropdown.spec.tsx | 106 + .../__tests__/KindFilterDropdown.spec.tsx | 130 + src/filters/__tests__/useSearchFilter.spec.ts | 94 + src/filters/const.ts | 41 + src/filters/filter-utils.ts | 107 + src/filters/index.ts | 6 + src/filters/useAllowEdgeCreation.ts | 6 + src/filters/useAppliedDisplayFilters.ts | 8 + src/filters/useDisplayFilters.ts | 14 + src/filters/useSearchFilter.ts | 25 + src/filters/useShowLabel.tsx | 15 + src/imgs/event-source.svg | 162 + src/models/EndPointSliceModel.ts | 23 + src/models/ServiceBindingModel.ts | 17 + src/models/application.ts | 14 + src/models/gitops-primer.ts | 14 + src/models/index.ts | 2 + .../operator-data-transformer.spec.ts | 219 + src/operators/actions/index.ts | 4 + src/operators/actions/serviceBindings.ts | 72 + .../components/OperatorBackedService.scss | 60 + .../components/OperatorBackedService.tsx | 138 + .../components/OperatorBackedServiceGroup.tsx | 156 + .../components/OperatorBackedServiceNode.tsx | 49 + src/operators/components/const.ts | 7 + .../components/operatorsComponentFactory.ts | 33 + src/operators/index.ts | 24 + .../listView/OperatorGroupListViewNode.tsx | 41 + src/operators/operatorFilters.ts | 39 + src/operators/operatorResources.ts | 29 + src/operators/operators-data-transformer.ts | 164 + src/operators/operatorsDataModelReconciler.ts | 168 + src/operators/operatorsTopologyPlugin.ts | 86 + src/redux/action.ts | 35 + src/redux/const.ts | 1 + src/redux/reducer.ts | 33 + src/resource-dropdown/ResourceDropdown.tsx | 282 + .../ResourceDropdownField.tsx | 91 + .../components/DropdownItem.tsx | 31 + src/resource-dropdown/utils/types.ts | 33 + .../usePreferredTopologyView.ts | 10 + src/utils/__tests__/application-utils.spec.ts | 289 + src/utils/__tests__/connector-utils.spec.ts | 84 + src/utils/__tests__/export-app-utils.spec.ts | 40 + src/utils/__tests__/test-resource-data.ts | 3525 +++++ .../__tests__/topology-knative-test-data.ts | 844 ++ src/utils/alert-utils.ts | 28 + src/utils/application-utils.ts | 252 + src/utils/common-utils.ts | 186 + src/utils/connector-utils.ts | 316 + src/utils/contexts/FileUploadContext.ts | 14 + .../contexts/ToastContext/ToastContext.tsx | 5 + .../contexts/ToastContext/utils/types.ts | 45 + src/utils/createConnection.tsx | 16 + src/utils/datetime.ts | 148 + src/utils/debounce.ts | 26 + src/utils/deployment-factory.ts | 159 + src/utils/dev-console-resources.ts | 50 + src/utils/export-app-utils.ts | 27 + src/utils/fetch-dynamic-eventsources-utils.ts | 14 + .../withHandlePromise.tsx | 59 + .../isOperatorBackedKnResource.ts | 16 + .../isOperatorBackedResource/utils/utils.ts | 18 + src/utils/hooks/knative/usePodsForRevision.ts | 91 + .../hooks/perspective/useLastPerspective.ts | 17 + .../perspective/usePerspectiveExtension.ts | 15 + .../hooks/perspective/usePerspectives.ts | 106 + .../perspective/usePreferredPerspective.ts | 12 + .../useValuesForPerspectiveContext.ts | 43 + src/utils/hooks/perspective/utils/const.ts | 6 + src/utils/hooks/perspective/utils/types.ts | 29 + src/utils/hooks/perspective/utils/utils.ts | 40 + .../useBuildsConfigWatcher.ts | 52 + .../useBuildsConfigWatcher/utils/const.ts | 4 + .../useBuildsConfigWatcher/utils/types.ts | 221 + .../useBuildsConfigWatcher/utils/utils.ts | 150 + .../hooks/useDaemonSetSideBarDetails.tsx | 20 + src/utils/hooks/useDebounceCallback.ts | 18 + src/utils/hooks/useDragDropContext.ts | 15 + src/utils/hooks/useFormikValidationFix.ts | 15 + .../hooks/usePodRingLabel/usePodRingLabel.ts | 35 + .../hooks/usePodRingLabel/utils/types.ts | 39 + .../hooks/usePodRingLabel/utils/utils.ts | 190 + .../hooks/usePodsWatcher/usePodsWatcher.ts | 63 + src/utils/hooks/usePodsWatcher/utils/utils.ts | 139 + .../usePrometheusRulesPoll.ts | 22 + .../usePrometheusRulesPoll/utils/const.ts | 6 + .../usePrometheusRulesPoll/utils/utils.ts | 58 + src/utils/hooks/usePromise.tsx | 41 + src/utils/hooks/useQueryParams.ts | 9 + .../hooks/useRelatedHPA/useRelatedHPA.ts | 44 + src/utils/hooks/useRelatedHPA/utils/utils.ts | 14 + src/utils/hooks/useTelemetry/useTelemetry.ts | 34 + src/utils/hooks/useTelemetry/utils/const.ts | 2 + src/utils/hooks/useTelemetry/utils/utils.ts | 12 + src/utils/hooks/useToast.ts | 5 + src/utils/hooks/useTopologyTranslation.ts | 13 + .../useUserSettingsCompatibility.ts | 32 + .../utils/utils.ts | 10 + src/utils/humanize.js | 432 + src/utils/icon-utils.ts | 7 + src/utils/index.ts | 10 + src/utils/isMultiClusterEnabled.ts | 4 + src/utils/k8s-utils.ts | 139 + src/utils/knative-models.ts | 254 + src/utils/knative-topology-utils.ts | 215 + src/utils/knative/data-transformer.ts | 188 + .../fetch-dynamic-eventsources-utils.ts | 8 + src/utils/knative/get-knative-resources.ts | 82 + src/utils/knative/knative-const.ts | 54 + src/utils/knative/knative-topology-utils.ts | 1278 ++ .../knativeListViewNodeComponentFactory.ts | 48 + src/utils/label-selector.ts | 284 + src/utils/logos.ts | 200 + src/utils/metricStats.ts | 45 + src/utils/models.ts | 16 + src/utils/moveNodeToGroup.tsx | 66 + src/utils/operator-utils.ts | 39 + src/utils/pod-utils.ts | 323 + src/utils/prometheus-utils.ts | 33 + src/utils/reducer.ts | 6 + src/utils/relationship-provider-utils.ts | 80 + src/utils/removeConnection.tsx | 31 + src/utils/resource-link-utils.ts | 59 + src/utils/resource-utils.ts | 626 + src/utils/resources/shared.ts | 34 + src/utils/selector-requirement.ts | 100 + src/utils/selector-utils.ts | 38 + src/utils/styles/bootstrap-variables.scss | 57 + src/utils/styles/topology-styles.scss | 21 + src/utils/styles/vars.scss | 76 + src/utils/swagger-utils.ts | 91 + src/utils/time-constants.ts | 5 + src/utils/topology-utils.ts | 233 + src/utils/truncate-utils.ts | 49 + src/utils/types/commonTypes.ts | 127 + src/utils/types/dev-console-types.ts | 10 + src/utils/types/hpaTypes.ts | 108 + src/utils/types/images.d.ts | 9 + src/utils/types/index.d.ts | 17 + src/utils/types/jsonSchema7Types.ts | 83 + src/utils/types/k8s-types.ts | 187 + src/utils/types/knativeTypes.ts | 144 + src/utils/types/kubevirtTypes.ts | 153 + src/utils/types/metric-types.ts | 8 + src/utils/types/modal-context.ts | 5 + src/utils/types/podTypes.ts | 169 + src/utils/types/prometheus-types.ts | 14 + src/utils/types/resource-types.ts | 22 + src/utils/types/swagger-types.ts | 23 + src/utils/types/topology-types.ts | 253 + src/utils/useMetricStats.ts | 21 + src/utils/useOverviewMetrics.ts | 9 + src/utils/validation-schema-utils.ts | 96 + src/utils/withEditReviewAccess.tsx | 31 + src/utils/workload-pause-utils.ts | 27 + tsconfig.json | 23 + webpack.config.ts | 165 + yarn.lock | 10656 ++++++++++++++++ 705 files changed, 69786 insertions(+) create mode 100644 .deepsource.toml create mode 100644 .eslintignore create mode 100644 .eslintrc.json create mode 100644 .gitignore create mode 100644 .prettierrc create mode 100644 Dockerfile create mode 100644 INTERNATIONALIZATION.md create mode 100644 OWNERS create mode 100644 console-extensions.json create mode 100644 i18next-parser.config.mjs create mode 100755 images/logos/3scale.svg create mode 100755 images/logos/aerogear.svg create mode 100644 images/logos/amq.svg create mode 100755 images/logos/angularjs.svg create mode 100755 images/logos/ansible.svg create mode 100755 images/logos/apache.svg create mode 100755 images/logos/beaker.svg create mode 100644 images/logos/build-icon.svg create mode 100644 images/logos/camel.svg create mode 100755 images/logos/capedwarf.svg create mode 100755 images/logos/cassandra.svg create mode 100644 images/logos/catalog-icon.svg create mode 100755 images/logos/clojure.svg create mode 100755 images/logos/codeigniter.svg create mode 100644 images/logos/cordova.png create mode 100755 images/logos/datagrid.svg create mode 100755 images/logos/datavirt.svg create mode 100644 images/logos/debian.svg create mode 100755 images/logos/decisionserver.svg create mode 100755 images/logos/django.svg create mode 100644 images/logos/dotnet.svg create mode 100755 images/logos/drupal.svg create mode 100755 images/logos/eap.svg create mode 100755 images/logos/elastic.svg create mode 100755 images/logos/erlang.svg create mode 100644 images/logos/fedora.svg create mode 100644 images/logos/freebsd.svg create mode 100755 images/logos/git.svg create mode 100755 images/logos/github.svg create mode 100755 images/logos/gitlab.svg create mode 100755 images/logos/glassfish.svg create mode 100755 images/logos/go-gopher.svg create mode 100644 images/logos/golang.svg create mode 100755 images/logos/grails.svg create mode 100755 images/logos/hadoop.svg create mode 100755 images/logos/haproxy.svg create mode 100644 images/logos/helm.svg create mode 100755 images/logos/infinispan.svg create mode 100755 images/logos/jboss.svg create mode 100755 images/logos/jenkins.svg create mode 100755 images/logos/jetty.svg create mode 100755 images/logos/joomla.svg create mode 100755 images/logos/jruby.svg create mode 100755 images/logos/js.svg create mode 100644 images/logos/knative.svg create mode 100644 images/logos/kubevirt.svg create mode 100755 images/logos/laravel.svg create mode 100755 images/logos/load-balancer.svg create mode 100755 images/logos/mariadb.svg create mode 100755 images/logos/mediawiki.svg create mode 100755 images/logos/memcached.svg create mode 100755 images/logos/mongodb.svg create mode 100755 images/logos/mssql.svg create mode 100755 images/logos/mysql-database.svg create mode 100755 images/logos/nginx.svg create mode 100755 images/logos/nodejs.svg create mode 100755 images/logos/openjdk.svg create mode 100644 images/logos/openliberty.svg create mode 100644 images/logos/openshift.svg create mode 100755 images/logos/openstack.svg create mode 100644 images/logos/operator.svg create mode 100644 images/logos/other-linux.svg create mode 100644 images/logos/other-unknown.svg create mode 100755 images/logos/perl.svg create mode 100755 images/logos/phalcon.svg create mode 100755 images/logos/php.svg create mode 100755 images/logos/play.svg create mode 100755 images/logos/postgresql.svg create mode 100755 images/logos/processserver.svg create mode 100755 images/logos/python.svg create mode 100644 images/logos/quarkus.svg create mode 100755 images/logos/rabbitmq.svg create mode 100755 images/logos/rails.svg create mode 100644 images/logos/redhat.svg create mode 100755 images/logos/redis.svg create mode 100755 images/logos/rh-integration.svg create mode 100644 images/logos/rh-spring-boot.svg create mode 100755 images/logos/rh-tomcat.svg create mode 100755 images/logos/ruby.svg create mode 100755 images/logos/scala.svg create mode 100644 images/logos/serverlessfx.svg create mode 100755 images/logos/shadowman.svg create mode 100644 images/logos/spring-boot.svg create mode 100755 images/logos/spring.svg create mode 100755 images/logos/sso.svg create mode 100755 images/logos/stackoverflow.svg create mode 100644 images/logos/suse.svg create mode 100755 images/logos/symfony.svg create mode 100755 images/logos/tomcat.svg create mode 100644 images/logos/ubuntu.svg create mode 100644 images/logos/vertx.svg create mode 100755 images/logos/wildfly.svg create mode 100644 images/logos/windows.svg create mode 100755 images/logos/wordpress.svg create mode 100755 images/logos/xamarin.svg create mode 100755 images/logos/zend.svg create mode 100644 images/restricted-sign.svg create mode 100644 integration-tests/.eslintrc create mode 100644 integration-tests/OWNERS create mode 100644 integration-tests/cypress.json create mode 100644 integration-tests/features/e2e/topology-ci.feature create mode 100644 integration-tests/features/export-application/export-of-applicaton.feature create mode 100644 integration-tests/features/topology/horizontal-pod-autoscaler/action-on-hpa-admin-perspective.feature create mode 100644 integration-tests/features/topology/horizontal-pod-autoscaler/actions-on-hpa.feature create mode 100644 integration-tests/features/topology/horizontal-pod-autoscaler/hpa-topology-sidebar.feature create mode 100644 integration-tests/features/topology/pipelines-on-topology.feature create mode 100644 integration-tests/features/topology/quick-search-topology.feature create mode 100644 integration-tests/features/topology/resource-quota-warning.feature create mode 100644 integration-tests/features/topology/resources-groupings.feature create mode 100644 integration-tests/features/topology/topology-add-operator-backed-service.feature create mode 100644 integration-tests/features/topology/topology-application-grouping.feature create mode 100644 integration-tests/features/topology/topology-chart-area-visual.feature create mode 100644 integration-tests/features/topology/topology-connecting-workloads.feature create mode 100644 integration-tests/features/topology/topology-display-filter.feature create mode 100644 integration-tests/features/topology/topology-editing-app-node.feature create mode 100644 integration-tests/features/topology/topology-filter-bar-display.feature create mode 100644 integration-tests/features/topology/topology-in-context-add-options.feature create mode 100644 integration-tests/features/topology/topology-layout-save.feature create mode 100644 integration-tests/features/topology/topology-list-view.feature create mode 100644 integration-tests/features/topology/topology-toolbar-filter.feature create mode 100644 integration-tests/features/topology/topology-workload-sidebar.feature create mode 100644 integration-tests/features/topology/topoplogy-delete-workload.feature create mode 100644 integration-tests/features/topology/workloads-on-topology.feature create mode 100644 integration-tests/package.json create mode 100644 integration-tests/plugins/index.js create mode 100644 integration-tests/reporter-config.json create mode 100644 integration-tests/support/commands/hooks.ts create mode 100644 integration-tests/support/commands/index.ts create mode 100644 integration-tests/support/page-objects/chart-area-po.ts create mode 100644 integration-tests/support/page-objects/export-applications-po.ts create mode 100644 integration-tests/support/page-objects/hpa-po.ts create mode 100644 integration-tests/support/page-objects/topology-add-options-po.ts create mode 100644 integration-tests/support/page-objects/topology-po.ts create mode 100644 integration-tests/support/pages/export-application/export-applications.ts create mode 100644 integration-tests/support/pages/functions/add-secret.ts create mode 100644 integration-tests/support/pages/functions/chart-functions.ts create mode 100644 integration-tests/support/pages/topology/index.ts create mode 100644 integration-tests/support/pages/topology/topology-actions-page.ts create mode 100644 integration-tests/support/pages/topology/topology-edit-deployment.ts create mode 100644 integration-tests/support/pages/topology/topology-helper-page.ts create mode 100644 integration-tests/support/pages/topology/topology-page.ts create mode 100644 integration-tests/support/pages/topology/topology-side-pane-page.ts create mode 100644 integration-tests/support/step-definitions/common/topology.ts create mode 100644 integration-tests/support/step-definitions/export-application/export-of-application.ts create mode 100644 integration-tests/support/step-definitions/topology/application-groupings.ts create mode 100644 integration-tests/support/step-definitions/topology/chart-view.ts create mode 100644 integration-tests/support/step-definitions/topology/create-workloads.ts create mode 100644 integration-tests/support/step-definitions/topology/delete-workload.ts create mode 100644 integration-tests/support/step-definitions/topology/edit-workload.ts create mode 100644 integration-tests/support/step-definitions/topology/horizontal-pod-autoscaler/action-on-hpa.ts create mode 100644 integration-tests/support/step-definitions/topology/list-view.ts create mode 100644 integration-tests/support/step-definitions/topology/pipelines-on-topology.ts create mode 100644 integration-tests/support/step-definitions/topology/quick-search-topology.ts create mode 100644 integration-tests/support/step-definitions/topology/resource-quota-warning.ts create mode 100644 integration-tests/support/step-definitions/topology/topology-add-options.ts create mode 100644 integration-tests/support/step-definitions/topology/topology-filters.ts create mode 100644 integration-tests/support/step-definitions/topology/workload-sidebar.ts create mode 100644 integration-tests/support/test-data/bindable-resource1.yaml create mode 100644 integration-tests/support/test-data/hippo-postgres-cluster.yaml create mode 100644 integration-tests/support/test-data/postgres-operator-backed.yaml create mode 100644 integration-tests/support/test-data/redis-standalone.yaml create mode 100644 integration-tests/support/test-data/servicebinding-resource-label-selector.yaml create mode 100644 integration-tests/testData/resource-quota/resource-quota.yaml create mode 100644 integration-tests/tsconfig.json create mode 100644 jest.config.ts create mode 100644 locales/en/expNamespace.json create mode 100644 locales/en/plugin__topology-plugin.json create mode 100644 locales/en/public.json create mode 100644 oc-manifest.yaml create mode 100644 package.json create mode 100755 scripts/start-console.sh create mode 100644 src/__tests__/CloseButton/CloseButton.scss create mode 100644 src/__tests__/CloseButton/CloseButton.tsx create mode 100644 src/__tests__/DataModelProvider.spec.tsx create mode 100644 src/__tests__/Graph.spec.tsx create mode 100644 src/__tests__/TopologyPage.spec.tsx create mode 100644 src/__tests__/TopologyPageToolbar.spec.tsx create mode 100644 src/__tests__/TopologyShortcuts.spec.tsx create mode 100644 src/__tests__/TopologySideBar.spec.tsx create mode 100644 src/__tests__/graph-test-data.ts create mode 100644 src/__tests__/service-binding-test-data.ts create mode 100644 src/__tests__/topology-test-data.ts create mode 100644 src/__tests__/topology-utils.spec.ts create mode 100644 src/actions/TopologyActions.tsx create mode 100644 src/actions/__tests__/contextMenuActions.spec.ts create mode 100644 src/actions/contextMenuActions.tsx create mode 100644 src/actions/edgeActions.ts create mode 100644 src/actions/index.ts create mode 100644 src/actions/modify-application.ts create mode 100644 src/actions/nodeActions.ts create mode 100644 src/actions/provider.ts create mode 100644 src/behavior/index.ts create mode 100644 src/behavior/useHover.ts create mode 100644 src/behavior/withCreateConnector.tsx create mode 100644 src/components/ActionMenu/ActionMenu.tsx create mode 100644 src/components/ActionMenu/components/ActionMenuContent/ActionMenuContent.tsx create mode 100644 src/components/ActionMenu/components/ActionMenuContent/components/ActionMenuItem/ActionMenuItem.tsx create mode 100644 src/components/ActionMenu/components/ActionMenuContent/components/ActionMenuItem/components/AccessReviewActionItem.tsx create mode 100644 src/components/ActionMenu/components/ActionMenuContent/components/ActionMenuItem/components/ActionItem.tsx create mode 100644 src/components/ActionMenu/components/ActionMenuContent/utils/utils.ts create mode 100644 src/components/ActionMenu/components/ActionMenuToggle.tsx create mode 100644 src/components/ActionsMenu/components/ActionsMenuDropdown.tsx create mode 100644 src/components/application-panel/ApplicationGroupResource.tsx create mode 100644 src/components/application-panel/TopologyApplicationList.tsx create mode 100644 src/components/application-panel/TopologyApplicationResources.scss create mode 100644 src/components/application-panel/TopologyApplicationResources.tsx create mode 100644 src/components/application-panel/__tests__/ApplicationGroupResource.spec.tsx create mode 100644 src/components/application-panel/application-resource-link.tsx create mode 100644 src/components/application-panel/index.ts create mode 100644 src/components/application-panel/useApplicationResourceTabSection.tsx create mode 100644 src/components/common/CopyToClipboard.tsx create mode 100644 src/components/common/DeploymentConfigDetailsList.tsx create mode 100644 src/components/common/DeploymentDetailsList/DeploymentDetailsList.tsx create mode 100644 src/components/common/DeploymentDetailsList/components/RuntimeClass.tsx create mode 100644 src/components/common/DetailsItem/DetailsItem.tsx create mode 100644 src/components/common/DetailsItem/components/EditButton.tsx create mode 100644 src/components/common/DetailsItem/components/LinkifyExternal.tsx create mode 100644 src/components/common/DetailsItem/components/PropertyPath.tsx create mode 100644 src/components/common/ExternalLink.tsx create mode 100644 src/components/common/LabelList/LabelList.tsx create mode 100644 src/components/common/LabelList/components/Label.tsx create mode 100644 src/components/common/LabelsModal/BaseLabelsModal.tsx create mode 100644 src/components/common/LabelsModal/components/LabelsModal.tsx create mode 100644 src/components/common/LabelsModal/utils/SelectorInput.tsx create mode 100644 src/components/common/LabelsModal/utils/const.ts create mode 100644 src/components/common/ListPage.tsx create mode 100644 src/components/common/LoadingInline/LoadingInline.tsx create mode 100644 src/components/common/LoadingInline/components/Box.tsx create mode 100644 src/components/common/LoadingInline/components/Data.tsx create mode 100644 src/components/common/LoadingInline/components/EmptyBox.tsx create mode 100644 src/components/common/LoadingInline/components/LoadError.tsx create mode 100644 src/components/common/LoadingInline/components/Loading.tsx create mode 100644 src/components/common/LoadingInline/components/MsgBox.tsx create mode 100644 src/components/common/ManagedByOperatorLink/ManagedByOperatorLink.tsx create mode 100644 src/components/common/ManagedByOperatorLink/components/ManagedByOperatorResourceLink.tsx create mode 100644 src/components/common/ManagedByOperatorLink/utils/utils.ts create mode 100644 src/components/common/OwnerReferences.tsx create mode 100644 src/components/common/PageHeading/PageHeading.tsx create mode 100644 src/components/common/PageHeading/components/BreadCrumbs.tsx create mode 100644 src/components/common/PageHeading/utils/types.ts create mode 100644 src/components/common/PodDisruptionBudgetField/PodDisruptionBudgetField.tsx create mode 100644 src/components/common/PodDisruptionBudgetField/components/AvailabilityRequirement.tsx create mode 100644 src/components/common/PodDisruptionBudgetField/utils/utils.ts create mode 100644 src/components/common/PodRing/PodRing.scss create mode 100644 src/components/common/PodRing/PodRing.tsx create mode 100644 src/components/common/PodRing/utils/hooks/usePodScalingAccessStatus.ts create mode 100644 src/components/common/PodRingSet.tsx create mode 100644 src/components/common/PodTraffic.tsx create mode 100644 src/components/common/ResourceSummary/ResourceSummary.tsx create mode 100644 src/components/common/ResourceSummary/utils/utils.tsx create mode 100644 src/components/common/Selector.tsx create mode 100644 src/components/common/Selector/components/Requirement.tsx create mode 100644 src/components/common/StatusBox/StatusBox.tsx create mode 100644 src/components/common/StatusBox/components/AccessDenied.tsx create mode 100644 src/components/common/StatusBox/components/LoadingBox.tsx create mode 100644 src/components/common/radio/RadioGroup.tsx create mode 100644 src/components/common/radio/RadioInput.tsx create mode 100644 src/components/dev-console/CreateProjectButton.tsx create mode 100644 src/components/dev-console/CreateProjectListPage.tsx create mode 100644 src/components/dev-console/ProjectListPage.tsx create mode 100644 src/components/dev-console/ResourceQuotaAlert/ResourceQuotaAlert.tsx create mode 100644 src/components/dev-console/ResourceQuotaAlert/types/types.ts create mode 100644 src/components/dropdowns/ApplicationDropdown.tsx create mode 100644 src/components/dropdowns/ApplicationSelector.tsx create mode 100644 src/components/dropdowns/NamespaceBarApplicationSelector.tsx create mode 100644 src/components/dropdowns/__tests__/ApplicationSelector.spec.tsx create mode 100644 src/components/error/ErrorBoundary.tsx create mode 100644 src/components/error/ErrorBoundaryFC.tsx create mode 100644 src/components/error/ErrorBoundaryFallbackPage.tsx create mode 100644 src/components/error/ErrorDetailsBlock.tsx create mode 100644 src/components/error/ExpandCollapse.tsx create mode 100644 src/components/error/withFallback.tsx create mode 100644 src/components/export-app/ExportApplication.tsx create mode 100644 src/components/export-app/ExportApplicationModal.tsx create mode 100644 src/components/export-app/ExportViewLogButton.tsx create mode 100644 src/components/export-app/__tests__/ExportApplication.spec.tsx create mode 100644 src/components/export-app/__tests__/ExportApplicationModal.spec.tsx create mode 100644 src/components/export-app/__tests__/ExportViewLogButton.spec.tsx create mode 100644 src/components/export-app/__tests__/export-data.ts create mode 100644 src/components/export-app/types.ts create mode 100644 src/components/export-app/useExportAppFormToast.ts create mode 100644 src/components/formik-fields/BaseInputField.tsx create mode 100644 src/components/formik-fields/InputField/InputField.scss create mode 100644 src/components/formik-fields/InputField/InputField.tsx create mode 100644 src/components/formik-fields/utils/types.ts create mode 100644 src/components/graph-view/Topology.scss create mode 100644 src/components/graph-view/Topology.tsx create mode 100644 src/components/graph-view/TopologyControlBar.scss create mode 100644 src/components/graph-view/TopologyControlBar.tsx create mode 100644 src/components/graph-view/TopologyShortcuts.tsx create mode 100644 src/components/graph-view/components/ContextMenu.scss create mode 100644 src/components/graph-view/components/GraphComponent.scss create mode 100644 src/components/graph-view/components/GraphComponent.tsx create mode 100644 src/components/graph-view/components/NodeShadows.tsx create mode 100644 src/components/graph-view/components/RegroupHint.scss create mode 100644 src/components/graph-view/components/RegroupHint.tsx create mode 100644 src/components/graph-view/components/Shortcut/Shortcut.scss create mode 100644 src/components/graph-view/components/Shortcut/Shortcut.tsx create mode 100644 src/components/graph-view/components/Shortcut/components/ShortcutCommand.tsx create mode 100644 src/components/graph-view/components/ShortcutTable.tsx create mode 100644 src/components/graph-view/components/componentFactory.ts create mode 100644 src/components/graph-view/components/componentUtils.ts create mode 100644 src/components/graph-view/components/edges/AggregateEdge.scss create mode 100644 src/components/graph-view/components/edges/AggregateEdge.tsx create mode 100644 src/components/graph-view/components/edges/BaseEdge.scss create mode 100644 src/components/graph-view/components/edges/BaseEdge.tsx create mode 100644 src/components/graph-view/components/edges/ConnectsTo.scss create mode 100644 src/components/graph-view/components/edges/ConnectsTo.tsx create mode 100644 src/components/graph-view/components/edges/CreateConnector.tsx create mode 100644 src/components/graph-view/components/edges/ServiceBinding.scss create mode 100644 src/components/graph-view/components/edges/ServiceBinding.tsx create mode 100644 src/components/graph-view/components/edges/TrafficConnector.scss create mode 100644 src/components/graph-view/components/edges/TrafficConnector.tsx create mode 100644 src/components/graph-view/components/edges/index.ts create mode 100644 src/components/graph-view/components/groups/Application.scss create mode 100644 src/components/graph-view/components/groups/Application.tsx create mode 100644 src/components/graph-view/components/groups/ApplicationGroupExpanded.tsx create mode 100644 src/components/graph-view/components/groups/ApplicationNode.tsx create mode 100644 src/components/graph-view/components/groups/GroupNode.scss create mode 100644 src/components/graph-view/components/groups/GroupNode.tsx create mode 100644 src/components/graph-view/components/groups/GroupNodeAnchor.ts create mode 100644 src/components/graph-view/components/groups/ResourceKindsInfo.scss create mode 100644 src/components/graph-view/components/groups/ResourceKindsInfo.tsx create mode 100644 src/components/graph-view/components/groups/index.ts create mode 100644 src/components/graph-view/components/groups/utils/utils.ts create mode 100644 src/components/graph-view/components/index.ts create mode 100644 src/components/graph-view/components/nodeContextMenu.tsx create mode 100644 src/components/graph-view/components/nodes/BaseNode.scss create mode 100644 src/components/graph-view/components/nodes/BaseNode.tsx create mode 100644 src/components/graph-view/components/nodes/BindableNode.tsx create mode 100644 src/components/graph-view/components/nodes/PodSet/PodSet.tsx create mode 100644 src/components/graph-view/components/nodes/PodSet/utils/utils.ts create mode 100644 src/components/graph-view/components/nodes/PodStatus/PodStatus.scss create mode 100644 src/components/graph-view/components/nodes/PodStatus/PodStatus.tsx create mode 100644 src/components/graph-view/components/nodes/PodStatus/hooks/useForceUpdate.ts create mode 100644 src/components/graph-view/components/nodes/PodStatus/utils/utils.ts create mode 100644 src/components/graph-view/components/nodes/WorkloadNode.scss create mode 100644 src/components/graph-view/components/nodes/WorkloadNode.tsx create mode 100644 src/components/graph-view/components/nodes/decorators/BuildDecorator.tsx create mode 100644 src/components/graph-view/components/nodes/decorators/BuildDecoratorBubble.tsx create mode 100644 src/components/graph-view/components/nodes/decorators/Decorator.scss create mode 100644 src/components/graph-view/components/nodes/decorators/Decorator.tsx create mode 100644 src/components/graph-view/components/nodes/decorators/EditDecorator.tsx create mode 100644 src/components/graph-view/components/nodes/decorators/MonitoringAlertsDecorator.tsx create mode 100644 src/components/graph-view/components/nodes/decorators/UrlDecorator.tsx create mode 100644 src/components/graph-view/components/nodes/decorators/__tests__/Decorator.spec.tsx create mode 100644 src/components/graph-view/components/nodes/decorators/__tests__/URLDecorator.spec.tsx create mode 100644 src/components/graph-view/components/nodes/decorators/defaultDecorators.tsx create mode 100644 src/components/graph-view/components/nodes/decorators/defaultDecoratorsPlugin.ts create mode 100644 src/components/graph-view/components/nodes/decorators/getDefaultDecorators.ts create mode 100644 src/components/graph-view/components/nodes/decorators/getNodeDecorators.tsx create mode 100644 src/components/graph-view/components/nodes/decorators/index.ts create mode 100644 src/components/graph-view/components/nodes/index.ts create mode 100644 src/components/graph-view/components/nodes/nodeUtils.ts create mode 100644 src/components/graph-view/components/router-decorator-icon/components/CheIcon.tsx create mode 100644 src/components/graph-view/components/router-decorator-icon/route-decorator-icon.tsx create mode 100644 src/components/graph-view/components/router-decorator-icon/types/types.ts create mode 100644 src/components/graph-view/components/router-decorator-icon/utils/utils.tsx create mode 100644 src/components/graph-view/components/withTopologyContextMenu.tsx create mode 100644 src/components/graph-view/index.ts create mode 100644 src/components/graph-view/layouts/TopologyColaLayout.ts create mode 100644 src/components/graph-view/layouts/layoutFactory.ts create mode 100644 src/components/helm/HelmReleaseListViewNode.tsx create mode 100644 src/components/knative/KnativeRevisionListViewNode.tsx create mode 100644 src/components/knative/KnativeServiceListViewNode.tsx create mode 100644 src/components/knative/NoStatusListViewNode.tsx create mode 100644 src/components/knative/SinkURIListViewNode.tsx create mode 100644 src/components/knative/icons/EventSourceIcon.tsx create mode 100644 src/components/knative/icons/icon-utils.tsx create mode 100644 src/components/list-view/ListElementWrapper.tsx create mode 100644 src/components/list-view/TopologyListView.scss create mode 100644 src/components/list-view/TopologyListView.tsx create mode 100644 src/components/list-view/TopologyListViewAppGroup.tsx create mode 100644 src/components/list-view/TopologyListViewKindGroup.tsx create mode 100644 src/components/list-view/TopologyListViewNode.tsx create mode 100644 src/components/list-view/TopologyListViewUnassignedGroup.tsx create mode 100644 src/components/list-view/cells/AlertsCell.scss create mode 100644 src/components/list-view/cells/AlertsCell.tsx create mode 100644 src/components/list-view/cells/CpuCell.tsx create mode 100644 src/components/list-view/cells/GroupResourcesCell.tsx create mode 100644 src/components/list-view/cells/MemoryCell.scss create mode 100644 src/components/list-view/cells/MemoryCell.tsx create mode 100644 src/components/list-view/cells/MetricsCell.scss create mode 100644 src/components/list-view/cells/MetricsTooltip.tsx create mode 100644 src/components/list-view/cells/StatusCell.scss create mode 100644 src/components/list-view/cells/StatusCell.tsx create mode 100644 src/components/list-view/cells/TypedResourceBadgeCell.tsx create mode 100644 src/components/list-view/cells/index.ts create mode 100644 src/components/list-view/index.ts create mode 100644 src/components/list-view/list-view-utils.tsx create mode 100644 src/components/list-view/listViewComponentFactory.ts create mode 100644 src/components/metal3/NodeLink.tsx create mode 100644 src/components/modals/ConfigureUpdateStrategyModal/ConfigureUpdateStrategyModal.tsx create mode 100644 src/components/modals/ConfigureUpdateStrategyModal/components/ConfigureUpdateStrategy.tsx create mode 100644 src/components/modals/ConfigureUpdateStrategyModal/utils/utils.ts create mode 100644 src/components/modals/EditApplicationModal.tsx create mode 100644 src/components/modals/ErrorModal.tsx create mode 100644 src/components/modals/MoveConnectionModal.tsx create mode 100644 src/components/modals/ResourceLimitsModal.tsx create mode 100644 src/components/modals/ServiceBindingModal/ServiceBindingModal.tsx create mode 100644 src/components/modals/ServiceBindingModal/components/BindableServices/BindableServices.tsx create mode 100644 src/components/modals/ServiceBindingModal/components/BindableServices/utils/utils.ts create mode 100644 src/components/modals/ServiceBindingModal/components/CreateServiceBindingForm.tsx create mode 100644 src/components/modals/ServiceBindingModal/index.ts create mode 100644 src/components/modals/ServiceBindingModal/utils/utils.ts create mode 100644 src/components/modals/index.ts create mode 100644 src/components/page/DroppableTopology.tsx create mode 100644 src/components/page/DroppableTopology/utils/utils.ts create mode 100644 src/components/page/DroppableTopologyComponent.tsx create mode 100644 src/components/page/LimitExceededState.tsx create mode 100644 src/components/page/PageContents.tsx create mode 100644 src/components/page/TopologyDataRenderer.tsx create mode 100644 src/components/page/TopologyEmptyState.tsx create mode 100644 src/components/page/TopologyPage.tsx create mode 100644 src/components/page/TopologyPageToolbar.tsx create mode 100644 src/components/page/TopologyPageToolbar/hooks/useAddToProjectAccess/useAddToProjectAccess.ts create mode 100644 src/components/page/TopologyPageToolbar/hooks/useAddToProjectAccess/utils/utils.ts create mode 100644 src/components/page/TopologyPageToolbar/hooks/useIsMobile/useIsMobile.ts create mode 100644 src/components/page/TopologyPageToolbar/hooks/useIsMobile/utils/const.ts create mode 100644 src/components/page/TopologyView.scss create mode 100644 src/components/page/TopologyView.tsx create mode 100644 src/components/quick-search/TopologyQuickSearch.tsx create mode 100644 src/components/quick-search/TopologyQuickSearchButton.scss create mode 100644 src/components/quick-search/TopologyQuickSearchButton.tsx create mode 100644 src/components/quick-search/topology-quick-search-utils.tsx create mode 100644 src/components/side-bar/DefaultResourceSideBar.tsx create mode 100644 src/components/side-bar/SelectedEntityDetails.tsx create mode 100644 src/components/side-bar/TopologyEdgePanel.tsx create mode 100644 src/components/side-bar/TopologyEdgeResourcesPanel.tsx create mode 100644 src/components/side-bar/TopologyGroupResourceItem.tsx create mode 100644 src/components/side-bar/TopologyGroupResourceList.tsx create mode 100644 src/components/side-bar/TopologyGroupResourcesPanel.tsx create mode 100644 src/components/side-bar/TopologySideBar.tsx create mode 100644 src/components/side-bar/TopologySideBarContent.tsx create mode 100644 src/components/side-bar/TopologySideBarTabSection.scss create mode 100644 src/components/side-bar/TopologySideBarTabSection.tsx create mode 100644 src/components/side-bar/__tests__/TopologyEdgeResourcesPanel.spec.tsx create mode 100644 src/components/side-bar/__tests__/topology-edge-resources-panel-data.ts create mode 100644 src/components/side-bar/components/SideBarAlerts.tsx create mode 100644 src/components/side-bar/components/SideBarBody.tsx create mode 100644 src/components/side-bar/components/SideBarHeading.scss create mode 100644 src/components/side-bar/components/SideBarHeading.tsx create mode 100644 src/components/side-bar/providers/SideBarTabHookResolver.tsx create mode 100644 src/components/side-bar/providers/SideBarTabLoader.tsx create mode 100644 src/components/side-bar/providers/useDetailsResourceLink.tsx create mode 100644 src/components/side-bar/providers/useDetailsTab.tsx create mode 100644 src/components/side-bar/providers/useDetailsTabSection.tsx create mode 100644 src/components/svg/SvgBoxedText.tsx create mode 100644 src/components/svg/SvgCircledIcon.tsx create mode 100644 src/components/svg/SvgDropShadowFilter.tsx create mode 100644 src/components/svg/SvgResourceIcon.scss create mode 100644 src/components/svg/SvgResourceIcon.tsx create mode 100644 src/components/svg/__tests__/SvgBoxedText.spec.tsx create mode 100644 src/components/svg/__tests__/SvgResourceIcon.spec.tsx create mode 100644 src/components/svg/index.ts create mode 100644 src/components/utils/PromiseComponent.tsx create mode 100644 src/components/utils/__tests__/CheckResourceQuota.spec.tsx create mode 100644 src/components/utils/__tests__/mockData.ts create mode 100644 src/components/utils/checkResourceQuota.ts create mode 100644 src/components/utils/subscribeOverviewAlerts.ts create mode 100644 src/components/utils/subscribeOverviewMetrics.ts create mode 100644 src/components/visual-connector/edge-resource-link.tsx create mode 100644 src/components/visual-connector/index.ts create mode 100644 src/components/visual-connector/resource-tab-section.tsx create mode 100644 src/components/workload/BuildOverview/BuildOverview.tsx create mode 100644 src/components/workload/BuildOverview/components/BuildLogLink.tsx create mode 100644 src/components/workload/BuildOverview/components/BuildNumberLink.tsx create mode 100644 src/components/workload/BuildOverview/components/BuildOverviewItem.tsx create mode 100644 src/components/workload/BuildOverview/components/BuildOverviewList.tsx create mode 100644 src/components/workload/BuildOverview/components/BuildPipelineLogLink.tsx create mode 100644 src/components/workload/BuildOverview/components/BuildStatus.tsx create mode 100644 src/components/workload/BuildOverview/components/LogSnippet.tsx create mode 100644 src/components/workload/BuildOverview/components/StatusTitle.tsx create mode 100644 src/components/workload/BuildOverview/utils/types.ts create mode 100644 src/components/workload/BuildOverview/utils/utils.ts create mode 100644 src/components/workload/CronJobSideBarDetails.tsx create mode 100644 src/components/workload/DaemonSetDetailsList.tsx create mode 100644 src/components/workload/DaemonSetSideBarDetails.tsx create mode 100644 src/components/workload/DeploymentConfigSideBarDetails.tsx create mode 100644 src/components/workload/DeploymentSideBarDetails.tsx create mode 100644 src/components/workload/JobOverview/JobsOverview.tsx create mode 100644 src/components/workload/JobOverview/components/JobOverviewItem.tsx create mode 100644 src/components/workload/JobOverview/components/JobOverviewList.tsx create mode 100644 src/components/workload/JobOverview/components/SidebarSectionHeading.tsx create mode 100644 src/components/workload/JobSideBarDetails.tsx create mode 100644 src/components/workload/JobsTabSection/JobsTabSection.tsx create mode 100644 src/components/workload/JobsTabSection/hooks/useJobsForCronJobWatcher.ts create mode 100644 src/components/workload/JobsTabSection/hooks/useJobsSideBarTabSection.tsx create mode 100644 src/components/workload/NetworkingOverview.tsx create mode 100644 src/components/workload/PodDetailsList/PodDetailsList.tsx create mode 100644 src/components/workload/PodDetailsList/components/PodStatus.tsx create mode 100644 src/components/workload/PodDetailsList/components/PodStatusPopover.tsx create mode 100644 src/components/workload/PodDetailsList/utils/utils.ts create mode 100644 src/components/workload/PodResourceSummary.tsx create mode 100644 src/components/workload/PodSideBarDetails.tsx create mode 100644 src/components/workload/ResolveAdapter.ts create mode 100644 src/components/workload/StatefulSetSideBarDetails.tsx create mode 100644 src/components/workload/WorkloadPausedAlert/WorkloadPausedAlert.tsx create mode 100644 src/components/workload/build-tab-section.tsx create mode 100644 src/components/workload/index.ts create mode 100644 src/components/workload/network-tab-section.tsx create mode 100644 src/components/workload/pods-tab-section.tsx create mode 100644 src/components/workload/resource-alert.tsx create mode 100644 src/components/workload/utils.ts create mode 100644 src/components/workload/workload-resource-link.tsx create mode 100644 src/const.ts create mode 100644 src/data-transforms/DataModelExtension.tsx create mode 100644 src/data-transforms/DataModelProvider.tsx create mode 100644 src/data-transforms/ModelContext.ts create mode 100644 src/data-transforms/TopologyDataRetriever.tsx create mode 100644 src/data-transforms/__tests__/__snapshots__/data-transformer.spec.ts.snap create mode 100644 src/data-transforms/__tests__/data-transformer.spec.ts create mode 100644 src/data-transforms/__tests__/transform-utils.spec.ts create mode 100644 src/data-transforms/__tests__/updateModelFromFilters.spec.ts create mode 100644 src/data-transforms/__tests__/updateTopologyDataModel.spec.ts create mode 100644 src/data-transforms/data-transformer.ts create mode 100644 src/data-transforms/transform-utils.ts create mode 100644 src/data-transforms/updateModelFromFilters.ts create mode 100644 src/data-transforms/updateTopologyDataModel.ts create mode 100644 src/data-transforms/useMonitoringAlerts.tsx create mode 100644 src/data-transforms/useRoutesURL.ts create mode 100644 src/elements/BaseNode.ts create mode 100644 src/elements/OdcBaseEdge.ts create mode 100644 src/elements/OdcBaseNode.ts create mode 100644 src/elements/index.ts create mode 100644 src/elements/odcElementFactory.ts create mode 100644 src/extensions/index.ts create mode 100644 src/extensions/topology.ts create mode 100644 src/filters/FilterDropdown.scss create mode 100644 src/filters/FilterDropdown.tsx create mode 100644 src/filters/FilterProvider.tsx create mode 100644 src/filters/KindFilterDropdown.scss create mode 100644 src/filters/KindFilterDropdown.tsx create mode 100644 src/filters/NameLabelFilterDropdown.tsx create mode 100644 src/filters/TopologyFilterBar.scss create mode 100644 src/filters/TopologyFilterBar.tsx create mode 100644 src/filters/__tests__/FilterDropdown.spec.tsx create mode 100644 src/filters/__tests__/KindFilterDropdown.spec.tsx create mode 100644 src/filters/__tests__/useSearchFilter.spec.ts create mode 100644 src/filters/const.ts create mode 100644 src/filters/filter-utils.ts create mode 100644 src/filters/index.ts create mode 100644 src/filters/useAllowEdgeCreation.ts create mode 100644 src/filters/useAppliedDisplayFilters.ts create mode 100644 src/filters/useDisplayFilters.ts create mode 100644 src/filters/useSearchFilter.ts create mode 100644 src/filters/useShowLabel.tsx create mode 100644 src/imgs/event-source.svg create mode 100644 src/models/EndPointSliceModel.ts create mode 100644 src/models/ServiceBindingModel.ts create mode 100644 src/models/application.ts create mode 100644 src/models/gitops-primer.ts create mode 100644 src/models/index.ts create mode 100644 src/operators/__tests__/operator-data-transformer.spec.ts create mode 100644 src/operators/actions/index.ts create mode 100644 src/operators/actions/serviceBindings.ts create mode 100644 src/operators/components/OperatorBackedService.scss create mode 100644 src/operators/components/OperatorBackedService.tsx create mode 100644 src/operators/components/OperatorBackedServiceGroup.tsx create mode 100644 src/operators/components/OperatorBackedServiceNode.tsx create mode 100644 src/operators/components/const.ts create mode 100644 src/operators/components/operatorsComponentFactory.ts create mode 100644 src/operators/index.ts create mode 100644 src/operators/listView/OperatorGroupListViewNode.tsx create mode 100644 src/operators/operatorFilters.ts create mode 100644 src/operators/operatorResources.ts create mode 100644 src/operators/operators-data-transformer.ts create mode 100644 src/operators/operatorsDataModelReconciler.ts create mode 100644 src/operators/operatorsTopologyPlugin.ts create mode 100644 src/redux/action.ts create mode 100644 src/redux/const.ts create mode 100644 src/redux/reducer.ts create mode 100644 src/resource-dropdown/ResourceDropdown.tsx create mode 100644 src/resource-dropdown/ResourceDropdownField.tsx create mode 100644 src/resource-dropdown/components/DropdownItem.tsx create mode 100644 src/resource-dropdown/utils/types.ts create mode 100644 src/user-preferences/usePreferredTopologyView.ts create mode 100644 src/utils/__tests__/application-utils.spec.ts create mode 100644 src/utils/__tests__/connector-utils.spec.ts create mode 100644 src/utils/__tests__/export-app-utils.spec.ts create mode 100644 src/utils/__tests__/test-resource-data.ts create mode 100644 src/utils/__tests__/topology-knative-test-data.ts create mode 100644 src/utils/alert-utils.ts create mode 100644 src/utils/application-utils.ts create mode 100644 src/utils/common-utils.ts create mode 100644 src/utils/connector-utils.ts create mode 100644 src/utils/contexts/FileUploadContext.ts create mode 100644 src/utils/contexts/ToastContext/ToastContext.tsx create mode 100644 src/utils/contexts/ToastContext/utils/types.ts create mode 100644 src/utils/createConnection.tsx create mode 100644 src/utils/datetime.ts create mode 100644 src/utils/debounce.ts create mode 100644 src/utils/deployment-factory.ts create mode 100644 src/utils/dev-console-resources.ts create mode 100644 src/utils/export-app-utils.ts create mode 100644 src/utils/fetch-dynamic-eventsources-utils.ts create mode 100644 src/utils/higher-order-components/withHandlePromise.tsx create mode 100644 src/utils/hooks/isOperatorBackedResource/isOperatorBackedKnResource.ts create mode 100644 src/utils/hooks/isOperatorBackedResource/utils/utils.ts create mode 100644 src/utils/hooks/knative/usePodsForRevision.ts create mode 100644 src/utils/hooks/perspective/useLastPerspective.ts create mode 100644 src/utils/hooks/perspective/usePerspectiveExtension.ts create mode 100644 src/utils/hooks/perspective/usePerspectives.ts create mode 100644 src/utils/hooks/perspective/usePreferredPerspective.ts create mode 100644 src/utils/hooks/perspective/useValuesForPerspectiveContext.ts create mode 100644 src/utils/hooks/perspective/utils/const.ts create mode 100644 src/utils/hooks/perspective/utils/types.ts create mode 100644 src/utils/hooks/perspective/utils/utils.ts create mode 100644 src/utils/hooks/useBuildsConfigWatcher/useBuildsConfigWatcher.ts create mode 100644 src/utils/hooks/useBuildsConfigWatcher/utils/const.ts create mode 100644 src/utils/hooks/useBuildsConfigWatcher/utils/types.ts create mode 100644 src/utils/hooks/useBuildsConfigWatcher/utils/utils.ts create mode 100644 src/utils/hooks/useDaemonSetSideBarDetails.tsx create mode 100644 src/utils/hooks/useDebounceCallback.ts create mode 100644 src/utils/hooks/useDragDropContext.ts create mode 100644 src/utils/hooks/useFormikValidationFix.ts create mode 100644 src/utils/hooks/usePodRingLabel/usePodRingLabel.ts create mode 100644 src/utils/hooks/usePodRingLabel/utils/types.ts create mode 100644 src/utils/hooks/usePodRingLabel/utils/utils.ts create mode 100644 src/utils/hooks/usePodsWatcher/usePodsWatcher.ts create mode 100644 src/utils/hooks/usePodsWatcher/utils/utils.ts create mode 100644 src/utils/hooks/usePrometheusRulesPoll/usePrometheusRulesPoll.ts create mode 100644 src/utils/hooks/usePrometheusRulesPoll/utils/const.ts create mode 100644 src/utils/hooks/usePrometheusRulesPoll/utils/utils.ts create mode 100644 src/utils/hooks/usePromise.tsx create mode 100644 src/utils/hooks/useQueryParams.ts create mode 100644 src/utils/hooks/useRelatedHPA/useRelatedHPA.ts create mode 100644 src/utils/hooks/useRelatedHPA/utils/utils.ts create mode 100644 src/utils/hooks/useTelemetry/useTelemetry.ts create mode 100644 src/utils/hooks/useTelemetry/utils/const.ts create mode 100644 src/utils/hooks/useTelemetry/utils/utils.ts create mode 100644 src/utils/hooks/useToast.ts create mode 100644 src/utils/hooks/useTopologyTranslation.ts create mode 100644 src/utils/hooks/useUserSettingsCompatibility/useUserSettingsCompatibility.ts create mode 100644 src/utils/hooks/useUserSettingsCompatibility/utils/utils.ts create mode 100644 src/utils/humanize.js create mode 100644 src/utils/icon-utils.ts create mode 100644 src/utils/index.ts create mode 100644 src/utils/isMultiClusterEnabled.ts create mode 100644 src/utils/k8s-utils.ts create mode 100644 src/utils/knative-models.ts create mode 100644 src/utils/knative-topology-utils.ts create mode 100644 src/utils/knative/data-transformer.ts create mode 100644 src/utils/knative/fetch-dynamic-eventsources-utils.ts create mode 100644 src/utils/knative/get-knative-resources.ts create mode 100644 src/utils/knative/knative-const.ts create mode 100644 src/utils/knative/knative-topology-utils.ts create mode 100644 src/utils/knative/knativeListViewNodeComponentFactory.ts create mode 100644 src/utils/label-selector.ts create mode 100644 src/utils/logos.ts create mode 100644 src/utils/metricStats.ts create mode 100644 src/utils/models.ts create mode 100644 src/utils/moveNodeToGroup.tsx create mode 100644 src/utils/operator-utils.ts create mode 100644 src/utils/pod-utils.ts create mode 100644 src/utils/prometheus-utils.ts create mode 100644 src/utils/reducer.ts create mode 100644 src/utils/relationship-provider-utils.ts create mode 100644 src/utils/removeConnection.tsx create mode 100644 src/utils/resource-link-utils.ts create mode 100644 src/utils/resource-utils.ts create mode 100644 src/utils/resources/shared.ts create mode 100644 src/utils/selector-requirement.ts create mode 100644 src/utils/selector-utils.ts create mode 100644 src/utils/styles/bootstrap-variables.scss create mode 100644 src/utils/styles/topology-styles.scss create mode 100644 src/utils/styles/vars.scss create mode 100644 src/utils/swagger-utils.ts create mode 100644 src/utils/time-constants.ts create mode 100644 src/utils/topology-utils.ts create mode 100644 src/utils/truncate-utils.ts create mode 100644 src/utils/types/commonTypes.ts create mode 100644 src/utils/types/dev-console-types.ts create mode 100644 src/utils/types/hpaTypes.ts create mode 100644 src/utils/types/images.d.ts create mode 100644 src/utils/types/index.d.ts create mode 100644 src/utils/types/jsonSchema7Types.ts create mode 100644 src/utils/types/k8s-types.ts create mode 100644 src/utils/types/knativeTypes.ts create mode 100644 src/utils/types/kubevirtTypes.ts create mode 100644 src/utils/types/metric-types.ts create mode 100644 src/utils/types/modal-context.ts create mode 100644 src/utils/types/podTypes.ts create mode 100644 src/utils/types/prometheus-types.ts create mode 100644 src/utils/types/resource-types.ts create mode 100644 src/utils/types/swagger-types.ts create mode 100644 src/utils/types/topology-types.ts create mode 100644 src/utils/useMetricStats.ts create mode 100644 src/utils/useOverviewMetrics.ts create mode 100644 src/utils/validation-schema-utils.ts create mode 100644 src/utils/withEditReviewAccess.tsx create mode 100644 src/utils/workload-pause-utils.ts create mode 100644 tsconfig.json create mode 100644 webpack.config.ts create mode 100644 yarn.lock diff --git a/.deepsource.toml b/.deepsource.toml new file mode 100644 index 0000000..22ec4b5 --- /dev/null +++ b/.deepsource.toml @@ -0,0 +1,13 @@ +version = 1 + +test_patterns = [ + "cypress/tests/**", + "**/*.test.*" +] + +[[analyzers]] +name = "javascript" +enabled = true + + [analyzers.meta] + plugins = ["react"] \ No newline at end of file diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..6355c85 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +# Ignore artifacts: +dist/ +node_modules/ diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..6f8ad86 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,70 @@ +{ + "env": { + "browser": true, + "es2021": true + }, + "extends": [ + "plugin:react/recommended", + "plugin:react-hooks/recommended", + "plugin:@typescript-eslint/recommended", + "prettier" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaFeatures": { + "jsx": true + }, + "ecmaVersion": 12, + "sourceType": "module" + }, + "plugins": ["react", "@typescript-eslint", "react-hooks", "prettier", "simple-import-sort"], + "settings": { + "react": { + "version": "detect" + }, + "import/resolver": { + "typescript": {}, // this loads /tsconfig.json to eslint + "node": { + "extensions": [".js", ".jsx", ".ts", ".tsx"] + } + } + }, + "rules": { + "@typescript-eslint/no-explicit-any": "off", + "react/display-name": "off", + "react/prop-types": "off", + "prettier/prettier": [ + "error", + { + "endOfLine": "auto" + } + ], + "no-nested-ternary": "error", + "simple-import-sort/exports": "error", + "@typescript-eslint/no-shadow": ["error"], + "simple-import-sort/imports": [ + "warn", + { + "groups": [ + // Node.js builtins. You could also generate this regex if you use a `.js` config. + // For example: `^(${require("module").builtinModules.join("|")})(/|$)` + [ + "^(assert|buffer|child_process|cluster|console|constants|crypto|dgram|dns|domain|events|fs|http|https|module|net|os|path|punycode|querystring|readline|repl|stream|string_decoder|sys|timers|tls|tty|url|util|vm|zlib|freelist|v8|process|async_hooks|http2|perf_hooks)(/.*|$)" + ], + // Packages + ["^react", "^\\w"], + // Internal packages. + ["^(@|config/)(/*|$)"], + // Side effect imports. + ["^\\u0000"], + // Parent imports. Put `..` last. + ["^\\.\\.(?!/?$)", "^\\.\\./?$"], + // Other relative imports. Put same-folder imports and `.` last. + ["^\\./(?=.*/)(?!/?$)", "^\\.(?!/?$)", "^\\./?$"], + // Style imports. + ["^.+\\.s?css$"] + ] + } + ] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b766531 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +**/node_modules +npm-debug.log +yarn-debug.log +yarn-error.log +dist +**/.env +/coverage +/gui-test-screenshots +cypress/gui-test-screenshots +cypress/cypress-a11y-report.json +.DS_Store +.devcontainer/dev.env +console-extensions.md +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions +.idea/ +scripts/ca.crt +scripts/console-client-secret \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..f8d7ade --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "arrowParens": "always", + "printWidth": 100, + "singleQuote": true, + "trailingComma": "all", + "tabWidth": 2, + "semi": true +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..db49c80 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM registry.access.redhat.com/ubi8/nodejs-16 AS builder +USER root +RUN command -v yarn || npm i -g yarn + +COPY . /opt/app-root/src +WORKDIR /opt/app-root/src +RUN yarn install --frozen-lockfile --ignore-engines && yarn build + +FROM registry.access.redhat.com/ubi8/nginx-120 + +COPY --from=builder /opt/app-root/src/dist /usr/share/nginx/html +USER 1001 +CMD /usr/libexec/s2i/run \ No newline at end of file diff --git a/INTERNATIONALIZATION.md b/INTERNATIONALIZATION.md new file mode 100644 index 0000000..8f6639d --- /dev/null +++ b/INTERNATIONALIZATION.md @@ -0,0 +1,98 @@ +# OpenShift Internationalization + +#### i18next and react-i18next + +Internationalization is implemented with [i18next](https://www.i18next.com/) and [react-i18next](https://react.i18next.com/). + +The react-i18next library offers a [hook](https://react.i18next.com/latest/usetranslation-hook), [higher order component](https://react.i18next.com/latest/withtranslation-hoc), and [function](https://react.i18next.com/latest/trans-component) that can be used with React components. + +With these tools, we can translate text without having to worry about a key naming strategy. The text is used as the key. + +Internationalized text and translations are located in JSON files in a locales folder in each package, as well as a locales folder in the public folder. + +In general, these files shouldn't be manually updated, though sometimes plurals will need to be adjusted manually +after files are generated. + +To generate and update text files: +``` +yarn i18n +``` + +This command launches the [code parser](https://github.com/i18next/i18next-parser), generates JSON files containing English key:value pairs for all internationalized strings, and consolidates any English JSON files with identical names to avoid namespace conflicts in i18next. + +#### Scope +We are not able to translate all text in the application. Text located in backend code or non-Red-Hat-controlled development environments may not be accessible for translation. + +This may include items such as: +* Resource statuses (i.e. "Running") +* Events surfaced from Kubernetes +* Alerts +* Error messages displayed to the user or in logs +* Operators that surface informational messages +* Logging messages +* Monitoring dashboard chart titles and dropdowns that come from the config map dashboard definition + +Localizaton is not included in the CLI at this time, and bidirectional (right-to-left) text such as Hebrew and Arabic are out of scope. + +#### Internationalization guidelines + +* Any usage of i18next's `TFunction` (rather than react-i18next's `TFunction`) must be performed inside a function or component. +* Don't use backticks inside of a `TFunction`. Our code parser will not automatically pick up the keys that contain backticks. + +Examples: +``` +Bad: t(`public~Hello, it is now {{date}}`, { date: new Date() }) +Good: t('public~Hello, it is now {{date}}', { date: new Date() }) +``` + +* `aria-label`, `aria-placeholder`, `aria-roledescription`, and `aria-valuetext` should be internationalized. +* Use the query parameter `?pseudolocalization=true&lng=en` to see pseudolocalization on strings you've marked for internationalization. + * Pseudolocalization adds brackets around the text and makes it longer so you can test components with different text lengths. +* Make sure there are no missing key warnings in your browser's developer tools - missing keys will trigger errors in integration tests. The warning will show up as an error in the JavaScript console. +* When displaying a resource kind, you can hard-code it directly in the internationalized text or use the predefined label on the model for the kind. + * `model.labelPluralKey` contains the key for the internationalized kind name and must be wrapped in its own `TFunction`. Not all kinds have this attribute, so it is necessary to check for it first as shown below: +``` +model.labelPluralKey ? t(model.labelPluralKey) : model.labelPlural +``` +* While i18next extracts translation keys in runtime, i18next-parser (the tool we use to generate JSON files) doesn't run the code, so it can't interpolate values in these expressions: + +``` +t(key) +t('key' + id) +t(`key${id}`) +``` + +As a workaround, you should specify possible static values in comments anywhere in your file: +``` +// t('key_1') +// t('key_2') +t(key) +/* +t('key1') +t('key2') +*/ +t('key' + id) +``` + +* The optional i18nKey property on the [react-i18next Trans component](https://react.i18next.com/latest/trans-component) should only be used as a last resort. There is a known (rare) issue with the parser, where it will sometimes incorrectly generate a key that contains HTML tags. If this is the case, you will see a missingKey error in the developer tools of your browser or in our end to end tests. In this instance, the i18nKey prop should be used and made the same as the text being internationalized. HTML tags like `` can be used directly in the i18nKey prop. +* Write tests for pseudolocalized code in Cypress + +#### Translations + +OpenShift is currently translated into three languages: Chinese, Korean, and Japanese. + +Translation work is done by the Red Hat Globalization team. We send them updated files from the public and packages folders on a weekly or biweekly basis for the entire console and regularly import new translations. + +#### Adding support for a new language + +To add support for a new language to OpenShift: +1. Look up the [ISO 639-1 code](https://www.loc.gov/standards/iso639-2/php/code_list.php) for the new language. +2. Add the new language code to `./i18n-scripts/languages.sh` +3. Update the language switcher component (`./public/components/modals/language-preferences-modal.tsx`) to support the new language if translations are available. + +#### Utilities +We have added various scripts to help us automate internationalization-related tasks in OpenShift. + +These scripts can all be found in `./i18n-scripts`. + +For more information, please review the [README](./i18n-scripts/README.md). diff --git a/OWNERS b/OWNERS new file mode 100644 index 0000000..4b2b644 --- /dev/null +++ b/OWNERS @@ -0,0 +1,21 @@ +# DO NOT EDIT; this file is auto-generated using https://github.com/openshift/ci-tools. +# See the OWNERS docs: https://git.k8s.io/community/contributors/guide/owners.md + +approvers: + - tnisan + - yaacov + - vojtechszocs + - pcbailey + - metalice + - avivtur + - hstastna + - upalatucci +reviewers: + - tnisan + - yaacov + - metalice + - pcbailey + - avivtur + - vojtechszocs + - hstastna + - upalatucci diff --git a/console-extensions.json b/console-extensions.json new file mode 100644 index 0000000..8434289 --- /dev/null +++ b/console-extensions.json @@ -0,0 +1,259 @@ +[ + { + "type": "console.flag/model", + "properties": { + "model": { + "group": "primer.gitops.io", + "version": "v1alpha1", + "kind": "Export" + }, + "flag": "ALLOW_EXPORT_APP" + } + }, + { + "type": "console.redux-reducer", + "properties": { + "scope": "devconsole", + "reducer": { "$codeRef": "reduxReducer" } + } + }, + { + "type": "console.topology/details/tab", + "properties": { + "id": "topology-side-bar-tab-details", + "label": "%plugin__topology-plugin~Details%" + } + }, + { + "type": "console.topology/details/tab", + "properties": { + "id": "topology-side-bar-tab-resource", + "label": "%plugin__topology-plugin~Resources%" + } + }, + { + "type": "console.topology/details/tab-section", + "properties": { + "id": "topology-tab-section-deployment-config", + "tab": "topology-side-bar-tab-details", + "provider": { "$codeRef": "workload.useDeploymentConfigSideBarDetails" } + } + }, + { + "type": "console.topology/details/tab-section", + "properties": { + "id": "topology-tab-section-deployment", + "tab": "topology-side-bar-tab-details", + "provider": { "$codeRef": "workload.useDeploymentSideBarDetails" } + } + }, + { + "type": "console.topology/details/tab-section", + "properties": { + "id": "topology-tab-section-daemon-set", + "tab": "topology-side-bar-tab-details", + "provider": { "$codeRef": "workload.useDaemonSetSideBarDetails" } + } + }, + { + "type": "console.topology/details/tab-section", + "properties": { + "id": "topology-tab-section-stateful-set", + "tab": "topology-side-bar-tab-details", + "provider": { "$codeRef": "workload.useStatefulSetSideBarDetails" } + } + }, + { + "type": "console.topology/details/tab-section", + "properties": { + "id": "topology-tab-section-jobs", + "tab": "topology-side-bar-tab-details", + "provider": { "$codeRef": "workload.useJobSideBarDetails" } + } + }, + { + "type": "console.topology/details/tab-section", + "properties": { + "id": "topology-tab-section-cron-jobs", + "tab": "topology-side-bar-tab-details", + "provider": { "$codeRef": "workload.useCronJobSideBarDetails" } + } + }, + { + "type": "console.topology/details/tab-section", + "properties": { + "id": "topology-tab-section-pod", + "tab": "topology-side-bar-tab-details", + "provider": { "$codeRef": "workload.usePodSideBarDetails" } + } + }, + { + "type": "console.topology/details/tab-section", + "properties": { + "id": "topology-tab-section-pods-overview", + "tab": "topology-side-bar-tab-resource", + "provider": { "$codeRef": "workload.usePodsSideBarTabSection" } + } + }, + { + "type": "console.topology/details/tab-section", + "properties": { + "id": "topology-tab-section-jobs-overview", + "tab": "topology-side-bar-tab-resource", + "provider": { "$codeRef": "workload.useJobsSideBarTabSection" } + } + }, + { + "type": "console.topology/details/tab-section", + "properties": { + "id": "topology-tab-section-builds-overview", + "tab": "topology-side-bar-tab-resource", + "provider": { "$codeRef": "workload.useBuildsSideBarTabSection" } + } + }, + { + "type": "console.topology/details/tab-section", + "properties": { + "id": "topology-tab-section-network-overview", + "tab": "topology-side-bar-tab-resource", + "provider": { "$codeRef": "workload.useNetworkingSideBarTabSection" } + } + }, + { + "type": "console.topology/details/resource-link", + "properties": { + "priority": 0, + "link": { "$codeRef": "workload.getWorkloadResourceLink" } + } + }, + { + "type": "console.topology/adapter/pod", + "properties": { + "adapt": { + "$codeRef": "workload.podsAdapterForWorkloads" + } + } + }, + { + "type": "console.topology/adapter/pod", + "properties": { + "adapt": { + "$codeRef": "workload.podsAdapterForCronJobWorkload" + } + } + }, + { + "type": "console.topology/adapter/build", + "properties": { + "adapt": { + "$codeRef": "workload.buildsAdapterForWorkloads" + } + } + }, + { + "type": "console.topology/adapter/network", + "properties": { + "adapt": { + "$codeRef": "workload.networkAdapterForWorkloads" + } + } + }, + { + "type": "console.action/provider", + "properties": { + "contextId": "topology-actions", + "provider": { "$codeRef": "actions.useTopologyWorkloadActionProvider" } + } + }, + { + "type": "console.topology/details/resource-alert", + "properties": { + "id": "health-check-alert", + "contentProvider": { + "$codeRef": "workload.useHealthChecksAlert" + } + } + }, + { + "type": "console.topology/details/resource-alert", + "properties": { + "id": "resource-quota-alert", + "contentProvider": { + "$codeRef": "workload.useResourceQuotaAlert" + } + } + }, + { + "type": "console.context-provider", + "properties": { + "provider": { "$codeRef": "exportAppContext.ExportAppContextProvider" }, + "useValueHook": { "$codeRef": "exportAppContext.useExportAppFormToast" } + }, + "flags": { + "required": ["ALLOW_EXPORT_APP"] + } + }, + { + "type": "console.user-preference/item", + "properties": { + "id": "topology.preferredView", + "label": "%plugin__topology-plugin~Topology%", + "groupId": "general", + "description": "%plugin__topology-plugin~If a topology view is not selected, the console defaults to the last viewed.%", + "field": { + "type": "dropdown", + "userSettingsKey": "topology.preferredView", + "defaultValue": "latest", + "options": [ + { + "value": "latest", + "label": "%plugin__topology-plugin~Last viewed%" + }, + { "value": "graph", "label": "%plugin__topology-plugin~Graph%" }, + { "value": "list", "label": "%plugin__topology-plugin~List%" } + ] + }, + "insertBefore": "console.preferredCreateEditMethod", + "insertAfter": "console.preferredNamespace" + } + }, + { + "type": "console.topology/details/tab-section", + "properties": { + "id": "topology-tab-section-application", + "tab": "topology-side-bar-tab-resource", + "provider": { "$codeRef": "applicationSidebar.useApplicationPanelResourceTabSection" } + } + }, + { + "type": "console.topology/details/resource-link", + "properties": { + "priority": 100, + "link": { "$codeRef": "applicationSidebar.getApplicationPanelResourceLink" } + } + }, + { + "type": "console.topology/details/resource-link", + "properties": { + "priority": 10, + "link": { "$codeRef": "vcSidebar.getEdgeResourceLink" } + } + }, + { + "type": "console.topology/details/tab-section", + "properties": { + "id": "topology-tab-section-visual-connector", + "tab": "topology-side-bar-tab-resource", + "provider": { + "$codeRef": "vcSidebar.useVisualConnectorResourceTabSection" + } + } + }, + { + "type": "console.action/provider", + "properties": { + "contextId": "topology-actions", + "provider": { "$codeRef": "actions.useTopologyVisualConnectorActionProvider" } + } + } +] diff --git a/i18next-parser.config.mjs b/i18next-parser.config.mjs new file mode 100644 index 0000000..6720e0a --- /dev/null +++ b/i18next-parser.config.mjs @@ -0,0 +1,10 @@ +export default { + sort: true, + createOldCatalogs: false, + keySeparator: false, + locales: ['en'], + namespaceSeparator: '~', + reactNamespace: false, + useKeysAsDefaultValue: true, + defaultNamespace: 'plugin__topology-plugin', +}; diff --git a/images/logos/3scale.svg b/images/logos/3scale.svg new file mode 100755 index 0000000..a05577c --- /dev/null +++ b/images/logos/3scale.svg @@ -0,0 +1 @@ +Logo \ No newline at end of file diff --git a/images/logos/aerogear.svg b/images/logos/aerogear.svg new file mode 100755 index 0000000..083202f --- /dev/null +++ b/images/logos/aerogear.svg @@ -0,0 +1,48 @@ + + + + + + + + diff --git a/images/logos/amq.svg b/images/logos/amq.svg new file mode 100644 index 0000000..4f84837 --- /dev/null +++ b/images/logos/amq.svg @@ -0,0 +1 @@ +producticons_1017_RGB_Messaging final color \ No newline at end of file diff --git a/images/logos/angularjs.svg b/images/logos/angularjs.svg new file mode 100755 index 0000000..91cc37d --- /dev/null +++ b/images/logos/angularjs.svg @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/images/logos/ansible.svg b/images/logos/ansible.svg new file mode 100755 index 0000000..6285967 --- /dev/null +++ b/images/logos/ansible.svg @@ -0,0 +1,12 @@ + + + + + diff --git a/images/logos/apache.svg b/images/logos/apache.svg new file mode 100755 index 0000000..71d4850 --- /dev/null +++ b/images/logos/apache.svg @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/logos/beaker.svg b/images/logos/beaker.svg new file mode 100755 index 0000000..1325ac6 --- /dev/null +++ b/images/logos/beaker.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/logos/build-icon.svg b/images/logos/build-icon.svg new file mode 100644 index 0000000..5785f14 --- /dev/null +++ b/images/logos/build-icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/images/logos/camel.svg b/images/logos/camel.svg new file mode 100644 index 0000000..65f425a --- /dev/null +++ b/images/logos/camel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/logos/capedwarf.svg b/images/logos/capedwarf.svg new file mode 100755 index 0000000..12a0a53 --- /dev/null +++ b/images/logos/capedwarf.svg @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/logos/cassandra.svg b/images/logos/cassandra.svg new file mode 100755 index 0000000..4ad581d --- /dev/null +++ b/images/logos/cassandra.svg @@ -0,0 +1,182 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/logos/catalog-icon.svg b/images/logos/catalog-icon.svg new file mode 100644 index 0000000..03b4b02 --- /dev/null +++ b/images/logos/catalog-icon.svg @@ -0,0 +1 @@ +Asset 2 \ No newline at end of file diff --git a/images/logos/clojure.svg b/images/logos/clojure.svg new file mode 100755 index 0000000..fc12708 --- /dev/null +++ b/images/logos/clojure.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + diff --git a/images/logos/codeigniter.svg b/images/logos/codeigniter.svg new file mode 100755 index 0000000..bb62bf2 --- /dev/null +++ b/images/logos/codeigniter.svg @@ -0,0 +1,22 @@ + + + + + + diff --git a/images/logos/cordova.png b/images/logos/cordova.png new file mode 100644 index 0000000000000000000000000000000000000000..c38303ae10cb678ee343c9b25a6035fde8e5fce0 GIT binary patch literal 9713 zcmV7)RXBxz!Wi}iXP0{w)` zlP_6Y5dnCGm(Us_XbaB>AOt_XF^6nVy_UvF-mi{PovHT(xPVvAIRMq)$M&ZPAh+TF zSG9akFV?&he8D4_IpY9)oUw_j7VhcAns?hY`?KL*uNth!3=prq<6xrcOZzzMqWuC@+9^$)53 zQS}OR=871@MXem5-EJT9NTGiJb?=J!Qo9y_^2ad0^Lr79X9G280aSmVwmR5>7#>qKB1Ezc50y>?}Q-0^y7J;7=4B+YBwg8gfBl$C-U8>i9PGEdC z5E=a8H<8*URImM?0?ixv$OnKhe;HVfexc{Y^eoF5P9E=vwe>!Eo@1?*)inhO)yb--~T9wW}CuP?|hhSJn|Xu9n$TkABP9$>2^Cf3}5?YU%(C5Uq`3YiJv)+ z$QNeSYBm1q@BWE<9(n`Iiwn3g-Z@+eB+|s=!q4fY&}w&RHX8iI4}3c}Tz8C(%`E}t zgdkW44g-z@r0n_d`D4;wdU#(D)vK#3yy+e9 zcXnwu8#EdXVPd%NfM>^b{GF`ccDKtzZ+Zte96Kt{7C?sQdS$ll10jG@)KF1y`SL3N z>wo@tzT+Fd5;nKW&lrDxG&b<-;vRkk0ziCzU>RTn5D2pX-u~|6Z11+Y{^&9unNXZ} zQbVte0>JBJJBPHbwY=!|TUlCMh?skA8kU)K`)&V)+i$%|K$Vz8nH~WkNGc!o&Cbm5 zxBvP8pZjV5pTkRwtZ!_}vnB+=Hv_*7eBdL=4HTar_zp(Uu+W2Z3}p-$DP{hb!Rmp-%M1M7zdgvly(RaWJYsn8c>~D-tp0BU?qmdwT6L={EBmYe zv=~i3Vy)7EiH`2)@f=-VDjxumGD$eN$O|$4mIVMgfslK~QYZ$cs*d<{%uFcE&d!omD%2WHZo1)mx_J&J(gLO07-fBE8ASm{ z78mvb@F3^UUtnQ=4)2*31b_CCUwJ)5ig(_xY!yd+vXT*T3U9%L{XOz*#H)eA%K__X?a> z&%k@_FRaBn&rG9+cTNm9^%xx9N*yeA9CDA~LiX=T7$etaX+pc*Wqo^_G)bf{jPc<< z^y;!_B7l;y21BLZ1PtpN8!Rl$@%O*~iyT>A3Le2UVdJUu29gPxZw^WXUA?->i6?^Y z_uw^@YPhDnjg3NaB@+Yfc85-$lNWZsIH`+XTRZYdeI^F5-1o@xqPT=#@EM=P(Id+u1!8)R>R_taKr*4v|Lq5|`d3%Q@}FrAv-}Mh zDMMD4iLVhR2qO_zNE?rQ4s9W=`~9V{(hju~m1Hm^M($Z&{c&g@c1%kJ@CeYK+?N7> z{V(@Ysn(+F&7>~k-+U-lz~6E}^p5u)XJdO;Hd|!IgSloRcY2T?A6eY$9h5d>f`Ny& zL$^2mw#pogK3FQ{n-}IAlYsWe0~G}E*X8J-^DHkd?5lzMxp3hk^AZ=BI-g+T*g!I^ z&p-LuQ49VUkM5*vraMNO(Y>?l104?j6%koFM}vn z19+Af=l7++S^oN;@1a_)p%0tNCiq3*$m9T+%;%q6_rLc8Y;5n*s8^Lq#5gcRIUk8z ztp~V@UOG+8F$G9?evk>qM}X=Fl#)axjz}B5PZTg+7Fj%8=kqC>NzD1Iq1C zjMNe14P{Dg=!NC?u>3LGrMp;@|5GnAN|M(wb3rv6RV6rm>$(XtQpQK412Vwd5 zR{yR9STJFQm3hxKH(p}>t3ymI(g%oO+Y2C)PvbvW^fdT4-GSL z$(eHIjzUF0Nl~K^h@j!i#v~##N%YbohKh+l`_d3kPy@2Za56}NN_F~k2NRqrNXA(G z;`1Mz?my1v_AZTJ`5Rq$vGB#1;qmu)lFM!*u|XJ1fLJehWlZ^A-jqin2_{L*i1Kqy zB%npuVVbotq5IsVuw*_5%i zkO`1~o_??fDb5#IYePs?zO%tk56K_iYDhPPh@F@HfD|KrR_~_> z`#C9Ku(_4UqL=HCIv`fsc&X906hdu4o~uL3i6kt}&+*v4MEtYA`&WM8JHAmFsk@nR zd)&Q)zX4u41pwqhf5p_K=sfZGNxFrVV2F3R=^|6zXAT~O0nV0!I)RCrh$&;kH6hB^ z2Zr&6G)wV->oZ%%20-bxy-N25Jf(gINKy&yPFJp}WErb#>uhdqk!1;9#wPu^f4pxY zeULzsRutg<-$w&baMN|i`1^l*fG_&&Pvxc?u48j+d#nuV5&4BL>2x|j2K?ML0bs3V z7_G0b-{_oUN)pjo%~IK=TUwZxWKZvh7*HBGiTVd;2YA)$B{A;F(yW|F8g8p&g%Jiy zQb?%GAVONV?J-}#@Zjs;M5mKetM!K?I#|D1V~JkX1y+=zthjI8A5#qIL0CG4dD81 z)#F)eOBy|wq27_}B>pV|CbAh~@Q>#{`ugv7~Gn`5q1)p<6#WWc5JeLik-o1Hz$jK3_8d;jM+v<=d34=(P=3?6r=hbJA^(d4WD8&a! z9WmcY+j%g$@}-1)1eF?)W(liTuX5qp%hYOmP$jsh;rY?_v$jFLk^2t>wUgY1!%OoK zlP47aT0b@F6w%9H{&JJO>}4;zMz+q?v%ng##FXTn7ZtEy1o4$D9ZPLM^b40nsdItJ zs$qpWNFSoUTrBqEHoE#F4?tYMN}XpeUSfB5kJ*`~06@M=Q;bOzbVV`vIb}fMhb;^_ zG}6uU(JOy^Ae^2$5VX9!++A5&IcjD5*tBwSK)(Rr269Xp2V6kE4 zSy#RW=L7)CRL8&-sw$)W9nwydl&!68E?&GMfDNMGRcj=T8H(9OktoU58g_37+HK^X zF^D}w$yxz`Yz}q1UG?wBmj=Veo;;wGFcBrw0|4#d%y4f>rV0KoaOizjuw za^zE_DlkS)7GHe!vZ@yA@YNc2W`VU&{3Jg61xM)sZ~yIKDe+!;~b7y=SvMkRf33RR#w*}^jocjP_Bdh z_S((2apdlo^25Lm@HPMaPBtFCh1vBLT)PE{#0tj!n@N%_+-x)?#5_IYg|>k+2c-f| zPwNS2KAfZ8sF5V8q-tl`kX1iG{EGy@K$5Y2!wtM_CgJ}A8F1#N8|ggpIEF2Jk>fSF zLYW!^Kt+s>DQ)c7isRDdD|EZLoK}+64+sEny@gjSS9med0&Y7p%e!v6g_)Bl0F@wR za1SB5|Bl}xgGZG8f zZMS7>6(7an4H!>kOgUiyX@;%TU~!S_L%14fEX=Vtdx-kl3f@|L*h4Ps;+V-ntP}cm zOqQi=Zf>)Bb&X0T(_`o+fl3389_8i~QeYOisSa;Hb`&Z#S^aSs+cTWeq32uWp}R?qL=b>P%#v>I@#ia z0FM#~v!z9Btu73ta3h0S1UEBHB?x4ljU$NDLquRpuLT5@N-Kw}JQyooby!^%`>s(Ew(vhHj%t0eG3n{`F<%2i4;G?dU9^6rAM! z?e4W%xw?j|Mj5XvDDMGaRsje|JDr=wReJz1fFDZ&IPYaa^uoY-Uc?^2H1U8_2MvIe zz$c8)cSQ5ZmD+{9M z0dzD5FwGvob8-RS_{KN-Z2v#Tpi`4FK7Q*~YD@^J$^;Na-r_y&kg-EQ2vek~9N4TV zf&~OF%P9I|rC>0ISp4hf0IvACdCtn!b)56c{8E6(rWuWFzo1kHs#zw{K@5RL$pBdd z3v+Xl71VCGV?WndE0G$BK&0wif7 zKb!Jz{E+S0*znWPZ^GKfx+rD_j;M>{Rm3R3iNs2sASAd>Y?md6Mv>}kR zi`-K2Ayy&dNBMxF_m3f~CsHIGyjLG(5Q#gM zBy4PLOK4Uie}H5zhmdtqKqtku_T&YSyD05|#PO3>3)^i&-Ua94eT|r8F7~5kN{SgTeMs3=9V@Y z00pq)q1$Rv^qAl65*VDMRY-y)fY|UO)W*>5c18Cm#t`~&UIE}N6gkP>E}iXN)=}C3 z?EbZ;y@$(lkwRbzWRgK@Q)~{|2DWx&4B%S3ejhRboE**cVT@NQib8S&XJ?zTl4{jT zl>V(Nv}6Q|1aL(GJG<<5I&6~>#P3!PZhHq?v_i{8jJv!`va`j;&K|4G5XA3F+u^r1v3VB(00xJ!PsFOT zaWZHrOBf?R!}iW@boe4Vkt$(pN78L!S5`O&LS5io0e-(Pk@plkWURTihg8@?-cU9_ zcSxkqgfaiPZyPxKd`W;)6YBp+1vrv4<Bv{2P%^=zT8fe zL{gx`p$k)%079y_RRTDi%`rP$v@TrWab8IHn<^{MuCTar1uF)ig2s4A2IT3hgn`T; zdHBv*@knAa5fgp4&ElEUoO$XQ{^)<-!X9wpdA(6uR z+GQH|+{4dz;a9djbNAgxWqS=%6f#D}8cL;{o`#f8H)m;4VgTft3s^4W_DLz*X$NZq zM4Lk1``bt8{?V>rVOf|zgrtafI^uSPj9iltuYN7g;#(~m=gzWr|AYMEXZ&B5-*<{* z?|%<@YZpn(ET?sQFmj#l0%}c9W%Zcf*L5=d(6K3Z7lxO9!XKqeBy-k z$-~V6@o!9gW<-AP<)ua07+YN}1RxWv_(=`G2~>dE0iQ4t7OXFpvI`^|1>eUeheUH?)Z#XL;d*&zzB4X1CGLq z3J~<4zjRsLA<@}MhH*|+2!o`6!|&|iTYFS4U&I-N8DjjECn{ok{PU$I5iy+GB20cv z!x`#4rrjcd#uR4gc%?+Xmv{z7RL+s5r4rcvw13RXO(X$$Zgeu<6PV;LX%%@c5@4=p z0JU0@D(=0LLl_vbAvAI)wE}?9&UKsCV+Q#5!}IYC!b91OGs?GdDTs5uP%1N0csOIi z;TBOjJsZGUH-V@h*uDTL^J_v5|d&n#0Hw3oskpAVH@~bfuJ$xRXl*_8vq{!*7oZq zW6T6%N@D?8mdQ?7Q4~^y5zgWUTDWn9P}@Z9^2%58u`w%#pOaK|!-s;r0mM6yvjFls zn5Cr!iG~hh`~)iY)bkF2_rL%BoxAV8`wX!399I0mzQh>ugTo(Js#H>B#aoAuHiuA; zAQtZf+#_jS%ag(VCvVr)+`4Xr4HcL4-g_R?d+*6hdT&10aPykp)BJm}q3f#>U>2H^$;@bCrHo5j@Y@|CxmXbwL;BT|+}`E%6mJ1cT% z1ad#YbvnS;pLc@K&d$u%6kYq!tN|+07FWK-ZpBxRa4`$T#)=Dtoj)cYKxjb+$W#B( z5k=q_Qrmo{lzWLq;0GxJLWm`_D6@|71)%zOV*(mUM!iEt4X%VjiF80Sryw_P}-)Ej)Xo0?aSWBN`Z*qG^Fw+Md;pA+Q9V zdh?s#j6tC{D)5GF+>3YtO-YM$k2(qb0cF7)sNF}3N7p@W=$*27kxSu!K-974K3C@f zSj}mx3)rp!s3Z8sX@)Oh`{T?1(EHv!chOH}Z@uSu!@<67uNr_t33~g)_`k*xU8=WH zev?Ecg!>tB5?}qwmt(saGw_u6aFzmq2m0aT%##_8WC6y{+-A@j>iL5^A!6h6m1cAJ*nKc1l{_>aI z=H@0AicB5J!a0W!ERrrr)e|tjw~s+&&P&Z%t4w@wIltt;dTsc5TCCDC@V&L;2k@=H zdo)hdbjK_JXoq6yo8SDVQ)iIniw_G4>0|v(&z$qY@VVwG_wS+O?7Xjl?mqUk?SLO< z2ISpeukxSgkNf`rjLcN!N><=0zazMGtPNOA{yZ0%oVTbE5*L`M5>9-r_5aF4f>yOG zIuxxG%nKa*DK_7~CXg3e2efW>-RUteztJB_VqkRAA#YwQBU2^5nTxSu&66p_c) z)DB~H(*of5{JY=%Zi?YY%hzQ99{)255dVMr^3ERkh*$v+%P_i$PM{0?<_gvG?us2j z!xCu2SVQ2qY4gIr2aio{d>~)V>80AGU%A~eQ;&L$+>^2OhzhOr6^-brl=g$Kjn zcrSWavmjipt*x~ST#z}gDg=#+;On_NKR<83-~}&;4r>wwLXzxm!Qf-*R3YSpVhvw& zA^fa3aANa7Rm|D3kd!;aIYv39+zXg{AVt;?HK;w``C|oM`OfXzxBDKzwW>nUs0co| z1D7S^t&e@|V?!xK!wh^eI)+#Q>+2g5lea6N%LLIPe%}7x?!SKf#1nsU&ef%E>p%VJ zProBV_fi#tn*`qH)~4c5fBMtke)X$gtvKiKiBG=t^>2UgS#gN9i%q1+fP( z16lVjMF?tvPGC9r1VIYWW>YKHJmZisi;GKGrY)(71kEsPw^~mXyI?lJ z1^<-)TjhC5A%_ph;!^u=Sxy%9r`&O%6I2vhd|hH-^9(NLWt8v04E(;PrADlxr*K z0lb1#3x55?NdTO@@NSGsX5bkx^pu;8|5c8T2)9~0`y>E(-nD9+AZT{CbF|lv(@;pt zmvWg;j*j;7GLLcrfCYfI+YG$dcRKjN4}Os7a~SwoEl{iE2qG2J5~^77<$Z->p@@cA zm1~Cg?%zvT=%4|db3w3-U$ku8{C6#&$M6?i4|zqpAG;AR~Q zAO{f5r}@P%ez7Mna~c99CJ&W^CIBWOve5qbd%YpGZFQ;OG)miqfe!#~_R~WEQUQhA zW7pc*>ZsO&06@s0@vOirY5$AezvQo(dP0ag7xY*Y$lbkrHlr4a0o!7SzSP&SyDgwY4^G*4>iAbXEEoh$nG4eIQXgIom|K5`d zx}RhRv!OgMH30kAfL9U|HRJNC5G2wM5F-Gypn1z&GaVitwiq+Ui+_*O00D_|41V$A ztpM;*J_2n5&=EX6f&HJi|8*7u884_w$|8Wh$u>SlXiyKSmYI30@CV@i%tV7gzyJO3 z+l)m3Hy3RN(oH7}JYU=CbatI{A)7zKM^HF}q=J&9d*G{mR;T0*G*8&LDd3f4VryCk zKGI>*z09)o95In}Bybu+ziV}Mwa>s3=o7-S^x7Ir58sHTh<{&RUWNdSs23hI{U}d& z0L2%A_P?u@$DKeVaXzK&3*a;b@FAfJoI;Em_#1ZJT&_{k*cp#8@N zmsaxXAYmLBt5|A~ROqn_9kYoh&A?9=7Z=|sK-ORjOT~aA)%9HR{Np?iT+e0aY`3s9y7W~<04HowRbl6 z?%un5PhuiW1i-O#E>E>uZ6t}$!Tr4aT$9xIAAPMD`0~Bu(k7y`gTTJ!8YLaN%Q?R*Un{~25_)b@D%_~O3oC{08`hZrvWD;6z+4wWa@wk+`SMDzzC6tOjF=P z7SI9kF|~g6qqrTNo8W#$@Hrw8pv4480~lj0qxgA*jYPb_?|aV#hRz&-e$Mfj7w3%e zoS)AP6n4_Qz}n;||FUxvy;EWmKo97m8DOxSQ~yk0T#WI8vyZ8Z&+72#g?W1{%Y!>n{8tS}howp*&#DqvkG-`E3l4uPJU8 zX$a0~9{iocZ*MwH5d2nvrl1Z2PobN+F#pX`zZDxN12>eY;g8Wwgx?!9?XC%nBh_vt zhmfm}+^tICd8=LX#t_AIj(IxWj9ZR`8;r?0i0gl>`c0VTSPqx#IrtoQzFy7o{ow9z z5<72>=iq-}5B30&C}Kw(y{v}dFpu<00000NkvXXu0mjfBO9=- literal 0 HcmV?d00001 diff --git a/images/logos/datagrid.svg b/images/logos/datagrid.svg new file mode 100755 index 0000000..01aa5b6 --- /dev/null +++ b/images/logos/datagrid.svg @@ -0,0 +1 @@ +Logo \ No newline at end of file diff --git a/images/logos/datavirt.svg b/images/logos/datavirt.svg new file mode 100755 index 0000000..21a7e73 --- /dev/null +++ b/images/logos/datavirt.svg @@ -0,0 +1 @@ +Logo \ No newline at end of file diff --git a/images/logos/debian.svg b/images/logos/debian.svg new file mode 100644 index 0000000..83789e8 --- /dev/null +++ b/images/logos/debian.svg @@ -0,0 +1,39 @@ + + + + +debian + + + + + + + + + + + + + diff --git a/images/logos/decisionserver.svg b/images/logos/decisionserver.svg new file mode 100755 index 0000000..8df19a6 --- /dev/null +++ b/images/logos/decisionserver.svg @@ -0,0 +1 @@ +Logo \ No newline at end of file diff --git a/images/logos/django.svg b/images/logos/django.svg new file mode 100755 index 0000000..7206611 --- /dev/null +++ b/images/logos/django.svg @@ -0,0 +1,37 @@ + + + + + + diff --git a/images/logos/dotnet.svg b/images/logos/dotnet.svg new file mode 100644 index 0000000..d204a09 --- /dev/null +++ b/images/logos/dotnet.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/images/logos/drupal.svg b/images/logos/drupal.svg new file mode 100755 index 0000000..39515e5 --- /dev/null +++ b/images/logos/drupal.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + diff --git a/images/logos/eap.svg b/images/logos/eap.svg new file mode 100755 index 0000000..33b408c --- /dev/null +++ b/images/logos/eap.svg @@ -0,0 +1 @@ +Logo \ No newline at end of file diff --git a/images/logos/elastic.svg b/images/logos/elastic.svg new file mode 100755 index 0000000..5f4a663 --- /dev/null +++ b/images/logos/elastic.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + diff --git a/images/logos/erlang.svg b/images/logos/erlang.svg new file mode 100755 index 0000000..1bf3933 --- /dev/null +++ b/images/logos/erlang.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + diff --git a/images/logos/fedora.svg b/images/logos/fedora.svg new file mode 100644 index 0000000..343198a --- /dev/null +++ b/images/logos/fedora.svg @@ -0,0 +1,23 @@ + + + + + + + + + diff --git a/images/logos/freebsd.svg b/images/logos/freebsd.svg new file mode 100644 index 0000000..fdf0661 --- /dev/null +++ b/images/logos/freebsd.svg @@ -0,0 +1 @@ +freebsd \ No newline at end of file diff --git a/images/logos/git.svg b/images/logos/git.svg new file mode 100755 index 0000000..85fc710 --- /dev/null +++ b/images/logos/git.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + diff --git a/images/logos/github.svg b/images/logos/github.svg new file mode 100755 index 0000000..527cd84 --- /dev/null +++ b/images/logos/github.svg @@ -0,0 +1,15 @@ + + + + + + diff --git a/images/logos/gitlab.svg b/images/logos/gitlab.svg new file mode 100755 index 0000000..7ae9489 --- /dev/null +++ b/images/logos/gitlab.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + diff --git a/images/logos/glassfish.svg b/images/logos/glassfish.svg new file mode 100755 index 0000000..6a0737e --- /dev/null +++ b/images/logos/glassfish.svg @@ -0,0 +1,60 @@ + + + + + + + + + + + diff --git a/images/logos/go-gopher.svg b/images/logos/go-gopher.svg new file mode 100755 index 0000000..2ae50a9 --- /dev/null +++ b/images/logos/go-gopher.svg @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/logos/golang.svg b/images/logos/golang.svg new file mode 100644 index 0000000..bccf226 --- /dev/null +++ b/images/logos/golang.svg @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/logos/grails.svg b/images/logos/grails.svg new file mode 100755 index 0000000..7a5acc3 --- /dev/null +++ b/images/logos/grails.svg @@ -0,0 +1,24 @@ + + + + + + diff --git a/images/logos/hadoop.svg b/images/logos/hadoop.svg new file mode 100755 index 0000000..c5f60ff --- /dev/null +++ b/images/logos/hadoop.svg @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/logos/haproxy.svg b/images/logos/haproxy.svg new file mode 100755 index 0000000..c7d8076 --- /dev/null +++ b/images/logos/haproxy.svg @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/logos/helm.svg b/images/logos/helm.svg new file mode 100644 index 0000000..5bd6605 --- /dev/null +++ b/images/logos/helm.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/images/logos/infinispan.svg b/images/logos/infinispan.svg new file mode 100755 index 0000000..cfbef0b --- /dev/null +++ b/images/logos/infinispan.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/logos/jboss.svg b/images/logos/jboss.svg new file mode 100755 index 0000000..cd6ffdc --- /dev/null +++ b/images/logos/jboss.svg @@ -0,0 +1,89 @@ + + + + + diff --git a/images/logos/jenkins.svg b/images/logos/jenkins.svg new file mode 100755 index 0000000..b859632 --- /dev/null +++ b/images/logos/jenkins.svg @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/logos/jetty.svg b/images/logos/jetty.svg new file mode 100755 index 0000000..eaad0a5 --- /dev/null +++ b/images/logos/jetty.svg @@ -0,0 +1,72 @@ + + + + + + + + + + diff --git a/images/logos/joomla.svg b/images/logos/joomla.svg new file mode 100755 index 0000000..a24662a --- /dev/null +++ b/images/logos/joomla.svg @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/images/logos/jruby.svg b/images/logos/jruby.svg new file mode 100755 index 0000000..6aa51d2 --- /dev/null +++ b/images/logos/jruby.svg @@ -0,0 +1,24 @@ + + + + + + + + + + diff --git a/images/logos/js.svg b/images/logos/js.svg new file mode 100755 index 0000000..463aa13 --- /dev/null +++ b/images/logos/js.svg @@ -0,0 +1,14 @@ + + + + + + + diff --git a/images/logos/knative.svg b/images/logos/knative.svg new file mode 100644 index 0000000..b86b6e3 --- /dev/null +++ b/images/logos/knative.svg @@ -0,0 +1 @@ +knative-logo-rgb2 diff --git a/images/logos/kubevirt.svg b/images/logos/kubevirt.svg new file mode 100644 index 0000000..7472103 --- /dev/null +++ b/images/logos/kubevirt.svg @@ -0,0 +1,96 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/images/logos/laravel.svg b/images/logos/laravel.svg new file mode 100755 index 0000000..777c41a --- /dev/null +++ b/images/logos/laravel.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + diff --git a/images/logos/load-balancer.svg b/images/logos/load-balancer.svg new file mode 100755 index 0000000..39ea851 --- /dev/null +++ b/images/logos/load-balancer.svg @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/images/logos/mariadb.svg b/images/logos/mariadb.svg new file mode 100755 index 0000000..edbeaa0 --- /dev/null +++ b/images/logos/mariadb.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + diff --git a/images/logos/mediawiki.svg b/images/logos/mediawiki.svg new file mode 100755 index 0000000..30e00d5 --- /dev/null +++ b/images/logos/mediawiki.svg @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/logos/memcached.svg b/images/logos/memcached.svg new file mode 100755 index 0000000..7df732a --- /dev/null +++ b/images/logos/memcached.svg @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/logos/mongodb.svg b/images/logos/mongodb.svg new file mode 100755 index 0000000..ddb570a --- /dev/null +++ b/images/logos/mongodb.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/logos/mssql.svg b/images/logos/mssql.svg new file mode 100755 index 0000000..7fb7859 --- /dev/null +++ b/images/logos/mssql.svg @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/images/logos/mysql-database.svg b/images/logos/mysql-database.svg new file mode 100755 index 0000000..dc69c09 --- /dev/null +++ b/images/logos/mysql-database.svg @@ -0,0 +1 @@ +mysql \ No newline at end of file diff --git a/images/logos/nginx.svg b/images/logos/nginx.svg new file mode 100755 index 0000000..1369ae0 --- /dev/null +++ b/images/logos/nginx.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + diff --git a/images/logos/nodejs.svg b/images/logos/nodejs.svg new file mode 100755 index 0000000..ae638fe --- /dev/null +++ b/images/logos/nodejs.svg @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/logos/openjdk.svg b/images/logos/openjdk.svg new file mode 100755 index 0000000..db17c75 --- /dev/null +++ b/images/logos/openjdk.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/logos/openliberty.svg b/images/logos/openliberty.svg new file mode 100644 index 0000000..007006c --- /dev/null +++ b/images/logos/openliberty.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/logos/openshift.svg b/images/logos/openshift.svg new file mode 100644 index 0000000..3f5c429 --- /dev/null +++ b/images/logos/openshift.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + diff --git a/images/logos/openstack.svg b/images/logos/openstack.svg new file mode 100755 index 0000000..b7d008f --- /dev/null +++ b/images/logos/openstack.svg @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/images/logos/operator.svg b/images/logos/operator.svg new file mode 100644 index 0000000..ca918b0 --- /dev/null +++ b/images/logos/operator.svg @@ -0,0 +1 @@ +Asset 4 \ No newline at end of file diff --git a/images/logos/other-linux.svg b/images/logos/other-linux.svg new file mode 100644 index 0000000..2a1a56c --- /dev/null +++ b/images/logos/other-linux.svg @@ -0,0 +1,686 @@ + + + + +other-linux + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/logos/other-unknown.svg b/images/logos/other-unknown.svg new file mode 100644 index 0000000..355bd95 --- /dev/null +++ b/images/logos/other-unknown.svg @@ -0,0 +1,16 @@ + + + + +other-unknown + + diff --git a/images/logos/perl.svg b/images/logos/perl.svg new file mode 100755 index 0000000..f8c5bd5 --- /dev/null +++ b/images/logos/perl.svg @@ -0,0 +1,57 @@ + + + + + + diff --git a/images/logos/phalcon.svg b/images/logos/phalcon.svg new file mode 100755 index 0000000..5ad8144 --- /dev/null +++ b/images/logos/phalcon.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/logos/php.svg b/images/logos/php.svg new file mode 100755 index 0000000..ea617d4 --- /dev/null +++ b/images/logos/php.svg @@ -0,0 +1 @@ +php \ No newline at end of file diff --git a/images/logos/play.svg b/images/logos/play.svg new file mode 100755 index 0000000..257f522 --- /dev/null +++ b/images/logos/play.svg @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/images/logos/postgresql.svg b/images/logos/postgresql.svg new file mode 100755 index 0000000..b80d8d3 --- /dev/null +++ b/images/logos/postgresql.svg @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/logos/processserver.svg b/images/logos/processserver.svg new file mode 100755 index 0000000..93c4732 --- /dev/null +++ b/images/logos/processserver.svg @@ -0,0 +1 @@ +Logo \ No newline at end of file diff --git a/images/logos/python.svg b/images/logos/python.svg new file mode 100755 index 0000000..e2d74f2 --- /dev/null +++ b/images/logos/python.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + diff --git a/images/logos/quarkus.svg b/images/logos/quarkus.svg new file mode 100644 index 0000000..6a828d6 --- /dev/null +++ b/images/logos/quarkus.svg @@ -0,0 +1 @@ +quarkus_icon_rgb_1024px_reverse diff --git a/images/logos/rabbitmq.svg b/images/logos/rabbitmq.svg new file mode 100755 index 0000000..1053289 --- /dev/null +++ b/images/logos/rabbitmq.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/images/logos/rails.svg b/images/logos/rails.svg new file mode 100755 index 0000000..890b3f1 --- /dev/null +++ b/images/logos/rails.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + diff --git a/images/logos/redhat.svg b/images/logos/redhat.svg new file mode 100644 index 0000000..d4ca42a --- /dev/null +++ b/images/logos/redhat.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/logos/redis.svg b/images/logos/redis.svg new file mode 100755 index 0000000..a1e677c --- /dev/null +++ b/images/logos/redis.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + diff --git a/images/logos/rh-integration.svg b/images/logos/rh-integration.svg new file mode 100755 index 0000000..498aad1 --- /dev/null +++ b/images/logos/rh-integration.svg @@ -0,0 +1 @@ +Logo \ No newline at end of file diff --git a/images/logos/rh-spring-boot.svg b/images/logos/rh-spring-boot.svg new file mode 100644 index 0000000..e37643b --- /dev/null +++ b/images/logos/rh-spring-boot.svg @@ -0,0 +1 @@ +snowdrop_icon_rgb_default diff --git a/images/logos/rh-tomcat.svg b/images/logos/rh-tomcat.svg new file mode 100755 index 0000000..50d4c33 --- /dev/null +++ b/images/logos/rh-tomcat.svg @@ -0,0 +1 @@ +Logo \ No newline at end of file diff --git a/images/logos/ruby.svg b/images/logos/ruby.svg new file mode 100755 index 0000000..08ec0d5 --- /dev/null +++ b/images/logos/ruby.svg @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/logos/scala.svg b/images/logos/scala.svg new file mode 100755 index 0000000..3b33043 --- /dev/null +++ b/images/logos/scala.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/logos/serverlessfx.svg b/images/logos/serverlessfx.svg new file mode 100644 index 0000000..a51d4cc --- /dev/null +++ b/images/logos/serverlessfx.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/logos/shadowman.svg b/images/logos/shadowman.svg new file mode 100755 index 0000000..0af1f89 --- /dev/null +++ b/images/logos/shadowman.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + diff --git a/images/logos/spring-boot.svg b/images/logos/spring-boot.svg new file mode 100644 index 0000000..8bb6343 --- /dev/null +++ b/images/logos/spring-boot.svg @@ -0,0 +1 @@ +springboot diff --git a/images/logos/spring.svg b/images/logos/spring.svg new file mode 100755 index 0000000..06f4127 --- /dev/null +++ b/images/logos/spring.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + diff --git a/images/logos/sso.svg b/images/logos/sso.svg new file mode 100755 index 0000000..644f5bc --- /dev/null +++ b/images/logos/sso.svg @@ -0,0 +1 @@ +Logo \ No newline at end of file diff --git a/images/logos/stackoverflow.svg b/images/logos/stackoverflow.svg new file mode 100755 index 0000000..aa2f0e2 --- /dev/null +++ b/images/logos/stackoverflow.svg @@ -0,0 +1,13 @@ + + + + + + + diff --git a/images/logos/suse.svg b/images/logos/suse.svg new file mode 100644 index 0000000..b203e4d --- /dev/null +++ b/images/logos/suse.svg @@ -0,0 +1,31 @@ + + + + +suse + + + + + + + + + diff --git a/images/logos/symfony.svg b/images/logos/symfony.svg new file mode 100755 index 0000000..a8f4733 --- /dev/null +++ b/images/logos/symfony.svg @@ -0,0 +1 @@ +symfony \ No newline at end of file diff --git a/images/logos/tomcat.svg b/images/logos/tomcat.svg new file mode 100755 index 0000000..325acd5 --- /dev/null +++ b/images/logos/tomcat.svg @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/logos/ubuntu.svg b/images/logos/ubuntu.svg new file mode 100644 index 0000000..d5f89b2 --- /dev/null +++ b/images/logos/ubuntu.svg @@ -0,0 +1,20 @@ + + + + +ubuntu + + + diff --git a/images/logos/vertx.svg b/images/logos/vertx.svg new file mode 100644 index 0000000..fa04b0f --- /dev/null +++ b/images/logos/vertx.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/logos/wildfly.svg b/images/logos/wildfly.svg new file mode 100755 index 0000000..322c7f0 --- /dev/null +++ b/images/logos/wildfly.svg @@ -0,0 +1,337 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/logos/windows.svg b/images/logos/windows.svg new file mode 100644 index 0000000..80bf02d --- /dev/null +++ b/images/logos/windows.svg @@ -0,0 +1,11 @@ + + + + +windows + + diff --git a/images/logos/wordpress.svg b/images/logos/wordpress.svg new file mode 100755 index 0000000..44b45b7 --- /dev/null +++ b/images/logos/wordpress.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + diff --git a/images/logos/xamarin.svg b/images/logos/xamarin.svg new file mode 100755 index 0000000..71d7262 --- /dev/null +++ b/images/logos/xamarin.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/images/logos/zend.svg b/images/logos/zend.svg new file mode 100755 index 0000000..5521850 --- /dev/null +++ b/images/logos/zend.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/images/restricted-sign.svg b/images/restricted-sign.svg new file mode 100644 index 0000000..c8dc4fc --- /dev/null +++ b/images/restricted-sign.svg @@ -0,0 +1,24 @@ + + + + restricted-sign + Created with Sketch. + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/integration-tests/.eslintrc b/integration-tests/.eslintrc new file mode 100644 index 0000000..3d42cef --- /dev/null +++ b/integration-tests/.eslintrc @@ -0,0 +1,6 @@ +{ + "rules": { + "promise/catch-or-return": "off", + "promise/no-nesting": "off" + } +} diff --git a/integration-tests/OWNERS b/integration-tests/OWNERS new file mode 100644 index 0000000..f6147f8 --- /dev/null +++ b/integration-tests/OWNERS @@ -0,0 +1,7 @@ +reviewers: + - hemantsaini-7 + - SparshKesari +approvers: + - psrna + - jrichter1 + - sanketpathak diff --git a/integration-tests/cypress.json b/integration-tests/cypress.json new file mode 100644 index 0000000..163aa9f --- /dev/null +++ b/integration-tests/cypress.json @@ -0,0 +1,33 @@ +{ + "integrationFolder": "features", + "testFiles": "**/*.{feature,features}", + "defaultCommandTimeout": 40000, + "viewportWidth": 1920, + "viewportHeight": 1080, + "watchForFileChanges": true, + "chromeWebSecurity": true, + "waitForAnimation": true, + "animationDistanceThreshold": 20, + "execTimeout": 90000, + "pageLoadTimeout": 90000, + "requestTimeout": 15000, + "responseTimeout": 15000, + "supportFile": "support/commands/index.ts", + "pluginsFile": "plugins/index.js", + "fixturesFolder": "testData", + "video": true, + "reporter": "../../../node_modules/cypress-multi-reporters", + "reporterOptions": { + "configFile": "reporter-config.json" + }, + "screenshotsFolder": "../../../gui_test_screenshots/cypress/screenshots", + "videosFolder": "../../../gui_test_screenshots/cypress/videos", + "env": { + "TAGS": "@topology and (@smoke or @regression or @pre-condition) and not (@manual or @to-do or @un-verified or @broken-test)", + "NAMESPACE": "aut-topology-ci" + }, + "retries": { + "runMode": 0, + "openMode": 0 + } +} diff --git a/integration-tests/features/e2e/topology-ci.feature b/integration-tests/features/e2e/topology-ci.feature new file mode 100644 index 0000000..32f26e6 --- /dev/null +++ b/integration-tests/features/e2e/topology-ci.feature @@ -0,0 +1,60 @@ +@topology @smoke +Feature: Perform actions on topology + User will be able to create workloads and perform actions on topology page + + + @pre-condition + Scenario: Background steps + Given user is at developer perspective + And user has created or selected namespace "aut-topology-ci" + + + Scenario: Empty state of topology: T-06-TC01 + When user navigates to Topology page + Then user sees Topology page with message "No resources found" + And user is able to see Start building your application, Add page links + And Display options dropdown, Filter by resource and Find by name fields are disabled + And switch view is disabled + + + Scenario: Build the application from topology page + Given user is at Topology Graph view + When user clicks Start building your application + And user enters ".NET" builder image in Quick Search bar + And user clicks Create application on Quick Search Dialog + And user enters Git Repo URL as "https://github.com/redhat-developer/s2i-dotnetcore-ex" in Create Source-to-Image Application + And user enters Application Name as "dotnet-app" + And user enters Name as "dotnet" + And user selects "Deployment" in Resource type section + And user clicks Create button on Create Source-to-Image Application page + Then user is able to see workload "dotnet" in topology page + + + Scenario Outline: Editing a workload: T-09-TC01 + Given user is at Topology Graph view + When user right clicks on the workload "" to open the Context Menu + And user clicks on "Edit " from context action menu + And user edits application groupings to "" + And user saves the changes + Then user can see application groupings updated to "" + + Examples: + | workload_name | application_groupings | + | dotnet | app | + + + Scenario: Default state of Display dropdown: T-16-TC01 + Given user is at Topology Graph view + When user clicks on the Display dropdown + Then user will see the Expand is checked + And user will see the Pod count is unchecked + And user will see the Labels is checked + + + Scenario: Deleting a workload through Action menu: T-15-TC01 + Given user is at Topology Graph view + When user clicks on workload "dotnet" + And user clicks on Action menu + And user clicks "Delete Deployment" from action menu + And user clicks on Delete button from modal + Then user will see workload disappeared from topology \ No newline at end of file diff --git a/integration-tests/features/export-application/export-of-applicaton.feature b/integration-tests/features/export-application/export-of-applicaton.feature new file mode 100644 index 0000000..f5fa29e --- /dev/null +++ b/integration-tests/features/export-application/export-of-applicaton.feature @@ -0,0 +1,77 @@ +@broken-test +# Gitops Primer Operator not installing correctly. +Feature: Export of application + As a user, I have an unmanaged application which I want to export. I'd like to be able to later add that code to git or some shared location so that I can share with others, or import into a new cluster or same cluster but different project, or be able to apply updates to an existing application. + + + + Background: + Given user has installed Gitops primer Operator + And user is at developer perspective + And user has created or selected namespace "aut-export-application" + + @regression @odc-6684 + Scenario: Export application button in topology: EA-02-TC01 + Given user has created "nodejs-ex-git-1" workload in "nodejs-ex-git-app" application + When user clicks on Export Application button + And user clicks on Ok button on Export Application modal to start the export + Then user can see a toast message saying "Export of resources in aut-export-application has started." + And user can see a toast message saying "All the resources are exported successfully from aut-export-application. Click below to download it." with download option and close button + And user can see primer deployment created in topology + + + @regression @odc-6296 + Scenario: Export Application modal when export application is in progress: EA-02-TC02 + Given user has created "nodejs-ex-git-2" workload in "nodejs-ex-git-app" application + When user clicks on Export Application button + And user clicks on Ok button on Export Application modal to start the export + And user clicks on Export Application button again + Then user can see "View logs" link, "Cancel Export", "Restart Export", and "Ok" button + + + + @regression @odc-6684 + Scenario: Restart Export when export application is in progress: EA-02-TC03 + Given user is at Topology page + When user clicks on Export Application button + And user clicks on Ok button on Export Application modal to start the export + And user clicks on Export Application button again + And user clicks on Restart button + Then user can see a toast message saying "Export of resources in aut-export-application has started." + And user can see a toast message saying "All the resources are exported successfully from aut-export-application. Click below to download it." with download option and close button + + + @regression @odc-6684 + Scenario: Cancel Export when export application is in progress: EA-02-TC04 + Given user is at Topology page + When user clicks on Export Application button + And user clicks on Ok button on Export Application modal to start the export + And user clicks on Export Application button again + And user clicks on Cancel button + Then user can see primer job gets deleted in topology + + + @regression @odc-6296 + Scenario: Export application button in empty topology view: EA-02-TC05 + Given user has created or selected namespace "aut-export-application-temp" + When user navigates to Topology page + Then user can see Export Application button disabled + + + @regression @manual @odc-6296 + Scenario: View logs when export application is in progress: EA-02-TC06 + Given user is at Topology page + When user clicks on Export Application button + And user clicks on Ok button on Export Application modal to start the export + And user clicks on Export Application button + And user clicks on View logs button on Export Application modal + Then user can see page showing the Pod log tab + + @regression @manual @odc-6296 + Scenario: Download the exported application: EA-02-TC07 + Given user is at Topology page + And Export Application is completed + And user clicks on download button from the toast message + And user clicks on "Log in with openshift" on the primer linked + And user clicks on "Allow selected permissions" on Authorize page + Then user can see a zip file is downloaded having the required file diff --git a/integration-tests/features/topology/horizontal-pod-autoscaler/action-on-hpa-admin-perspective.feature b/integration-tests/features/topology/horizontal-pod-autoscaler/action-on-hpa-admin-perspective.feature new file mode 100644 index 0000000..526712a --- /dev/null +++ b/integration-tests/features/topology/horizontal-pod-autoscaler/action-on-hpa-admin-perspective.feature @@ -0,0 +1,81 @@ +@topology +Feature: Perform actions on HPA in Administrative perspective + As a user, I want to edit the HPA assigned to a workload + + + Background: + Given user is at developer perspective + And user has created or selected namespace "topology-hpa" + And user has created a deployment workload "nodejs-ex-git" with CPU resource limit "100" and Memory resource limit "100" + And user is at administrator perspective + + + @regression @manual + Scenario Outline: Add HPA from Administrative perspective: TH-01-TC01 + Given user is at HorizontalPodAutoscaler page under workloads section + When user clicks Create HorizontalPodAutoscaler button + And user sees Create Horizontal Pod Autoscaler in YAML view + And user sees schema on the right sidebar + And user scrolls and checks schema + And user closes the schema + And user changes the metadata.name to "" + And user changes the spec.scaleTargetRef.name to "" + And user changes the spec.minReplicas to "" + And user changes the spec.maxReplicas to "" + And user changes value to spec.metrics.resource.target.averageUtilization under cpu target as "" + And user adds new field for memory similar to cpu under spec.metrics as resource.name with value memory + And user adds value for memory to spec.metrics.resource.target.averageUtilization under memory target as "" + And user sees create and cancel button + And user clicks on Save button + And user checks details with Action menu on top + And user sees Edit Labels, Edit Annotaions, Edit Horizontal Pod Autoscaler, Delete Horizontal Pod Autoscaler options in action menu + And user checks details, YAML and Events tabs + And user switches to developer perspective + And user opens the sidebar of the "" + And user selects on resource tab + Then user can see Horizontal Pod Autoscalers section + And user can see the "" with HPA tag associated present under HPA section + + Examples: + | workload_name | hpa_name | max_hpa_pod | min_hpa_pod | cpu_util | memory_util | + | nodejs-ex-git-1 | test-hpa | 5 | 2 | 60 | 30 | + + + @regression @manual + Scenario Outline: Edit HPA from Administrative perspective: TH-01-TC02 + Given user is at HorizontalPodAutoscaler page under workloads section + And user has created HorizontalPodAutoscaler + When user clicks the HPA associated with the workload + And user selects "Edit HorizontalPodAutoscaler" option from Actions menu in HPA details page + And user sees schema on the right sidebar + And user closes the schema + And user checks the spec.scaleTargetRef.name to be "" + And user changes the metadata.name to "" + And user clicks on Save button + And user sees error is thrown saying resource name cannot be changed + And user restores the previous name value i.e. "" + And user edits the spec.minReplicas to "" + And user edits the spec.maxReplicas to "" + And user edits value for cpu to spec.metrics.resource.target.averageUtilization under cpu target as "" + And user edits value for memory to spec.metrics.resource.target.averageUtilization under memory target as "" + And user sees save, reload and cancel button + And user clicks on Save button + And user sees YAML gets updated with updation message + And user clicks on details tab + Then user can see Min Replicas and Max Replicas updated value + And user can see Target value of resource memory and resource cpu to be the updated value + + Examples: + | workload_name | hpa_name | new_name_hpa | max_hpa_pod | min_hpa_pod | cpu_util | memory_util | + | nodejs-ex-git-1 | test-hpa | new-test-hpa | 7 | 4 | 75 | 60 | + + + @regression + Scenario: Delete HPA from Administrative perspective: TH-01-TC03 + Given user is at HorizontalPodAutoscaler page under workloads section + And user has created HorizontalPodAutoscaler + When user clicks the HPA associated with the workload + And user selects "Delete HorizontalPodAutoscaler" option from Actions menu in HPA details page + And user sees Delete Horizontal Pod Autoscaler modal opens + And user clicks Delete + Then user can see the intended HPA is deleted \ No newline at end of file diff --git a/integration-tests/features/topology/horizontal-pod-autoscaler/actions-on-hpa.feature b/integration-tests/features/topology/horizontal-pod-autoscaler/actions-on-hpa.feature new file mode 100644 index 0000000..91ee788 --- /dev/null +++ b/integration-tests/features/topology/horizontal-pod-autoscaler/actions-on-hpa.feature @@ -0,0 +1,129 @@ +@broken-test +# Not able to create HPA because of BUG: https://issues.redhat.com/browse/OCPBUGS-2306 +Feature: Perform actions on HPA in Topology page + As a user, I want to add HPA to a workload + + + Background: + Given user is at developer perspective + And user has created or selected namespace "topology-hpa" + + + @regression + Scenario Outline: Add HorizontalPodAutoscaler to deployment workload: TH-02-TC01 + Given user has created a deployment workload "nodejs-ex-git" with CPU resource limit "100" and Memory resource limit "100" + And user is at Topology page + When user clicks on workload "nodejs-ex-git" + And user selects "Add HorizontalPodAutoscaler" option from Actions menu + And user enters Name as "" in Horizontal Pod Autoscaler page + And user sets Minimum Pods value to "" + And user sets Maximum Pods value to "" + And user enters CPU Utilization as "" + And user enters Memory Utilization as "" + And user clicks on Save button + Then user can see sidebar opens with Resources tab selected by default + And user can see two pods under pod section + And user can see HorizontalPodAutoscalers section + And user can see the "" with HPA tag associated present under HPA section + + Examples: + | hpa_name | hpa_min_pod | hpa_max_pod | cpu_utilisation | memory_utilization | + | test-hpa | 2 | 5 | 60 | 30 | + + + @regression + Scenario Outline: Edit HorizontalPodAutoscaler: TH-02-TC02 + Given user has a deployment workload "" with HPA assigned to it + And user is at Topology page + When user clicks on workload "" + And user selects "Edit HorizontalPodAutoscaler" option from Actions menu + And user sees values are prefilled + And user checks the name value but cannot edit it + And user checks and edit the value to "" for maximum pods + And user checks and edit the value to "" for minimum pods + And user checks and edit the cpu value to "" + And user checks and edit the memory value to "" + And user clicks on Save button + Then user can see sidebar opens with Resources tab selected by default + And user can see HorizontalPodAutoscalers section + And user goes to the details tab + And user can see the changed pods number + + Examples: + | workload_name | hpa_max_pod | hpa_min_pod | cpu_utilisation | memory_utilization | + | nodejs-ex-git-1 | 6 | 3 | 75 | 50 | + + + @regression + Scenario: Remove HorizontalPodAutoscaler: TH-02-TC03 + Given user is at Topology page + And user can see a deployment workload "nodejs-ex-git-1" created with HPA assigned to it + When user right clicks on workload "nodejs-ex-git-1" + And user selects "Remove HorizontalPodAutoscaler" option from context menu + And user clicks Remove on Remove HorizontalPodAutoscaler? modal + And user clicks on workload "nodejs-ex-git-1" + And user selects Resources tab on sidebar + Then user can not see HorizontalPodAutoscalers section + + + @regression @manual + Scenario: Add HPA from YAML view: TH-02-TC04 + When user clicks on workload "nodejs-ex-git" + And user selects "Add HorizontalPodAutoscaler" option from Actions menu + And user navigates to YAML view in Horizontal Pod Autoscaler page + And user sees schema on the right sidebar + And user scrolls and checks schema + And user closes the schema + And user checks the YAML + And user assigns name value as "test-hpa-1" + And user gives value to averageUtilization under cpu target as "70" + And user clicks on Save button + And user clicks on workload "nodejs-ex-git" + And user selects Resources tab on sidebar + Then user can see Horizontal Pod Autoscalers section + And user can see the "test-hpa-1" with HPA tag associated present under HPA section + + + @regression @manual + Scenario: Edit HPA action YAML view: TH-02-TC05 + Given user has a workload "nodejs-edit-hpa" with HPA assigned to it + When user opens sidebar of workload + And user opens action menu + And user clicks on Edit Horizontal Pod Autoscaler option + And user sees Edit Horizontal Pod Autoscaler page opens with form view selected + And user selects YAML view option + And user sees schema on the right sidebar + And user scrolls and checks schema + And user closes the schema + And user checks the YAML + And user checks and edits values of cpu under metrics.resource.target.averageUtilization to "70" + And user checks and edits values of memory under metrics.resource.target.averageUtilization to "50" + And user checks and edits minimum pods value to "3" in minReplicas + And user checks and edits maximum pods value to "6" in maxReplicas + And user checks the name value under metadata.name + And user clicks on save to get back to topology page + And user opens the sidebar + And user selects on resource tab + And user sees Horizontal Pod Autoscalers section + And user goes to the details tab + Then user can see the changed pods number + + + @regression @manual + Scenario: Edit HPA YAML view to form view: TH-02-TC06 + Given user has a workload "th-02-tc06" with HPA assigned to it + When user opens sidebar of workload + And user opens action menu + And user clicks on Edit Horizontal Pod Autoscaler option + And user sees Edit Horizontal Pod Autoscaler page opens with form view selected + And user selects YAML view option + And user sees the schema on the right sidebar + And user checks the schema + And user closes the schema window + And user checks the YAML + And user changes values of cpu under metrics.resource.target.averageUtilization with resource name as cpu to "75" + And user changes values of memory under metrics.resource.target.averageUtilization with resource name as memory to "50" + And user changes minimum pods value to "3" in minReplicas + And user changes maximum pods value to "6" in maxReplicas + And user switches to form view + Then user checks the changed values in form view \ No newline at end of file diff --git a/integration-tests/features/topology/horizontal-pod-autoscaler/hpa-topology-sidebar.feature b/integration-tests/features/topology/horizontal-pod-autoscaler/hpa-topology-sidebar.feature new file mode 100644 index 0000000..9b36e1a --- /dev/null +++ b/integration-tests/features/topology/horizontal-pod-autoscaler/hpa-topology-sidebar.feature @@ -0,0 +1,22 @@ +@topology +Feature: Add HPA action and topology sidebar modifications + As a user, I want to add a HPA to a workload + + Background: + Given user is at developer perspective + And user has created a deployment/deployment-config + And user has assigned values to cpu and memory value in resource limit section of advanced option + And user has a workload with HPA assigned to it + + @regression @manual + Scenario: Changes due to HPA in Workload Sidebar: TH-03-TC01 + Given user is in topology + When user opens sidebar of workload + And user selects on resource tab + And user sees Horizontal Pod Autoscalers section + And user opens action menu + And user does not see Edit Pod Count option + And user clicks on details tab + Then user can see the scaling of the pod disabled + And user can see the arrows to increase and decrease pods are not present + And user can see the pod value inside the pod donut diff --git a/integration-tests/features/topology/pipelines-on-topology.feature b/integration-tests/features/topology/pipelines-on-topology.feature new file mode 100644 index 0000000..5ef1085 --- /dev/null +++ b/integration-tests/features/topology/pipelines-on-topology.feature @@ -0,0 +1,115 @@ +@broken-test +# Pipelines operator not installing correctly + +Feature: Improve the integration of Pipelines & Builds. + As a user, I want to see pipelines instead of build + + Background: + Given user has installed OpenShift Pipelines Operator + And user is at developer perspective + And user has created or selected namespace "aut-topology" + + + @regression + Scenario Outline: Pipelines are getting executed successfully: T-01-TC01 + Given user has created workload "" with resource type "" with pipeline + When user goes to the pipelines page + Then user can see the "" pipeline is succeeded + + Examples: + | resource_type | workload_name | + | deployment | nodejs-ex-git-1 | + | deployment config | dancer-ex-git-1 | + + + @regression + Scenario Outline: PVC getting created through the add flow pipeline auto start feature: T-01-TC02 + Given user has created workload "" with resource type "" with pipeline + When user goes to the Administrator perspective + And user clicks on the Persistent Volume Claims in Storage tab + Then user can see workspace created for the resource + + Examples: + | resource_type | workload_name | + | deployment | nodejs-ex-git-1 | + | deployment config | dancer-ex-git-1 | + + + @regression + Scenario Outline: PVC getting auto selected using the pipeline label attached to it: T-01-TC03 + Given user has created workload "" with resource type "" with pipeline + When user goes to the pipelines page + And user clicks on Start on the "" pipeline + Then user can see "PVC" in workspace with name of PVC + + Examples: + | resource_type | workload_name | + | deployment | nodejs-ex-git-1 | + | deployment config | dancer-ex-git-1 | + + + @regression + Scenario Outline: In Add trigger, PVC getting auto selected: T-01-TC04 + Given user has created workload "" with resource type "" with pipeline + When user goes to the pipelines page + And user clicks on Add Trigger on the "" pipeline + Then user can see "PVC" in workspace with name of PVC + + Examples: + | resource_type | workload_name | + | deployment | nodejs-ex-git-1 | + | deployment config | dancer-ex-git-1 | + + + @regression + Scenario Outline: Pipeline section in edit flow when pipeline is already present: T-01-TC05 + Given user has created workload "" with resource type "" with pipeline + When user clicks on Edit "" from action menu + Then user can see Pipeline checkbox is disabled + And user can not see Build configuration option in Advanced Options + + Examples: + | resource_type | workload_name | + | deployment | nodejs-ex-git-1 | + | deployment config | dancer-ex-git-1 | + + + @regression + Scenario Outline: Pipeline section in edit flow when pipeline is not present: T-01-TC06 + Given user has created workload "" with resource type "" without pipeline + When user clicks on Edit "" from action menu + Then user can see Pipeline section is present + And user can see Pipeline checkbox is present in enabled state + + Examples: + | resource_type | workload_name | + | deployment | nodejs-ex-git-2 | + | deployment config | dancer-ex-git-2 | + + + @regression + Scenario Outline: Pipeline is enabled through edit flow: T-01-TC07 + Given user has created workload "" with resource type "" without pipeline + When user clicks on Edit "" from action menu + And user checks the Pipeline checkbox to disable build configuration in Advanced Options + And user clicks on Save button + Then user can see PipelineRuns section is present + And user can see Build section is present + + Examples: + | resource_type | workload_name | + | deployment | nodejs-ex-git-2 | + | deployment config | dancer-ex-git-2 | + + + @regression @odc-6375 + Scenario Outline: Topology sidebar has Triggers section in Resources tab: T-01-TC8 + Given user has created workload "" with resource type "" with pipeline + When user navigates to Topology page + And user clicks on workload "" to open sidebar + Then user can see "Triggers" section in Resources tab + + Examples: + | resource_type | workload_name | + | deployment | nodejs-ex-git-1 | + | deployment config | dancer-ex-git-1 | diff --git a/integration-tests/features/topology/quick-search-topology.feature b/integration-tests/features/topology/quick-search-topology.feature new file mode 100644 index 0000000..ec94d46 --- /dev/null +++ b/integration-tests/features/topology/quick-search-topology.feature @@ -0,0 +1,125 @@ +@topology +Feature: Provide quick search from topology/list views to add to project + As a user, I should be able to have a quick way to search for items to add to my application/project in the Topology List/Graph view + + Background: + Given user is at developer perspective + And user has created or selected namespace "aut-topology" + + + @regression + Scenario: Add to project button in topology graph view: T-02-TC01 + Given user created a workload and is at topology list view + When user clicks on graph view button + Then user can see Add to project button + + + @regression + Scenario: Add to project button in topology list view: T-02-TC02 + Given user is at topology graph view + When user clicks on list view button + Then user can see Add to project button + + + @regression + Scenario: Add to project bar in topology graph view: T-02-TC03 + Given user is at topology graph view + When user clicks Add to project button + Then user can see Add to project search bar + + + @regression + Scenario: Add to project bar in topology list view: T-02-TC04 + Given user is at topology list view + When user clicks Add to project button + Then user can see Add to project search bar + + + @regression + Scenario: Add django application in topology chart view: T-02-TC05 + Given user is at topology graph view + When user clicks Add to project button + And user enters "django" in Add to project search bar + And user selects django+PostgreSQL option + And user clicks on instantiate template + And user clicks on create with default values in Instantiate Template form + Then user can see "PostgreSQL" and "django" workload in topology graph view + + @regression + Scenario: Add .Net application in topology list view: T-02-TC06 + Given user is at topology list view + When user clicks Add to project button + And user enters "net" in Add to project search bar + And user selects .Net core option + And user clicks on Create Application + And user clicks on create with default values in Create Application form + Then user can see "dotnet" workload in topology list view + + + @regression + Scenario: View all results option for django in topology graph view: T-02-TC07 + Given user is at topology graph view + When user clicks Add to project button + And user enters "django" in Add to project search bar + And user clicks on View all developer catalog items link + Then user will see Catalog with "django" text filter + + + @regression + Scenario: No results for the search: T-02-TC08 + Given user is at topology graph view + When user clicks Add to project button + And user enters "abcdef" in Add to project search bar + Then user will see No results + + + @regression + Scenario: Quick Add of Quick Starts in topology graph view: T-02-TC09 + Given user is at topology graph view + When user clicks Add to project button + And user enters "monitor your sample application" in Add to project search bar + And user selects Monitor your sample application option + And user clicks on Start button + Then Monitor your sample application quick start displays in the Topology + + + @regression + Scenario: View all Quick Starts option for pipeline in topology graph view: T-02-TC10 + Given user is at topology graph view + When user clicks Add to project button + And user enters "pipeline" in Add to project search bar + And user clicks on View all quick starts link + Then user will be redirected to the search results of the Quick Starts Catalog + + + @regression + Scenario: Quick Add of Devfile in topology graph view: T-02-TC011 + Given user is at topology graph view + When user clicks Add to project button + And user enters "node" in Add to project search bar + And user selects Basic NodeJS Devfiles option + And user clicks on Create Application + And user enters Name as "devfile-sample-git" in Import from Devfile page + And user clicks on Create button in the Import from Devfile page + Then user is taken to the Topology page with "devfile-sample-git" workload created + + + @regression + Scenario: Quick Add of Devfile Sample in topology graph view: T-02-TC012 + Given user is at topology graph view + When user clicks Add to project button + And user enters "node" in Add to project search bar + And user selects Basic NodeJS Samples option + And user clicks on Create Devfile Samples + And user enters Name as "basic-nodejs-sample-ex1" in Import from Devfile page + And user clicks on Create button in the Import from Devfile page + Then user is taken to the Topology page with "basic-nodejs-sample-ex1" workload created + + + @regression + Scenario: View all Samples option for node in topology graph view: T-02-TC13 + Given user is at topology graph view + When user clicks Add to project button + And user enters "node" in Add to project search bar + And user clicks on View all Samples link + Then user is taken to the search results in context of the Samples page \ No newline at end of file diff --git a/integration-tests/features/topology/resource-quota-warning.feature b/integration-tests/features/topology/resource-quota-warning.feature new file mode 100644 index 0000000..347b23b --- /dev/null +++ b/integration-tests/features/topology/resource-quota-warning.feature @@ -0,0 +1,49 @@ +@topology @ODC6771 +Feature: Update user in topology page if Quotas has been reached in a namespace + If any resource reached resource quota limit, a warning alert will be displayed for the user in Topology page. + + Background: + Given user is at developer perspective + And user has created or selected namespace "aut-topology-resource-quota" + + + @regression + Scenario: single resource reached quota: T-19-TC01 + Given user has created workload with resource type deployment + When user creates resource quota 'resourcequota1' by entering 'testData/resource-quota/resource-quota.yaml' file data + And user navigates to Topology page + And user clicks on link to view resource quota details + Then user is redirected to resource quota details page + + + @regression + Scenario: multiple resources reached quota: T-19-TC02 + Given user has created workload with resource type deployment + And user has created two resource quotas using 'testData/resource-quota/resource-quota.yaml' file + When user navigates to Topology page + And user clicks on link to view resource quota details + Then user is redirected to resource quota list page + + + @regression + Scenario: deployment node has yellow border around it and side-panel shows alert when resource quota is reached: T-19-TC03 + Given user is at Add page + And user has created workload "ex-node-js1" with resource type "deployment" + And user is at Topology page + When user clicks on workload 'ex-node-js1' + And user can see sidebar opens with Resources tab selected by default + Then user is able to see resource quota alert + And user is able to see yellow border around 'ex-node-js1' workload + + @regression + Scenario: checking background color of deployment node for resource quota alert: T-19-TC04 + Given user is at Topology page + When user continously clicks on zoom-out button until it gets maximum zoomed out + Then user is able to see yellow background on workload for resource quota alert + + + @regression + Scenario: deployment node shows alert when resource quota is reached in list view: T-19-TC05 + Given user is at the Topology page + When user clicks on List view button + Then user will see resource quota alert in 'ex-node-js1' list node \ No newline at end of file diff --git a/integration-tests/features/topology/resources-groupings.feature b/integration-tests/features/topology/resources-groupings.feature new file mode 100644 index 0000000..59d58c8 --- /dev/null +++ b/integration-tests/features/topology/resources-groupings.feature @@ -0,0 +1,142 @@ +@topology +Feature: Resources Groupings in Topology + User will be able to show and hide all types of resources on Topology graph and list view + + + Background: + Given user is at developer perspective + And user has created or selected namespace "aut-topology" + + + @regression + Scenario: Default state of Resources dropdown: T-03-TC01 + Given user has created a deployment workload named "node-ex" + And user is at Topology page + When user clicks on the Resources dropdown + Then user sees that all the checkboxes are unchecked + + @regression @manual + Scenario: Ability to show Deployment resource types in Topology graph and list view: T-03-TC02 + Given user is at Topology page + When user clicks on the Resources dropdown + And user checks Deployments checkbox + And user unchecks other checkboxes if there are any + Then user will see the workloads of Deployments resource types only + + + @regression @manual + Scenario: Ability to hide Deployment resource types in Topology graph and list view: T-03-TC03 + Given user is at Topology page + When user clicks on the Resources dropdown + And user checks all checkboxes + And user unchecks Deployments checkbox + Then user will not see the workloads of Deployments resource types only + + + @regression @manual + Scenario: Ability to show Deployment Config resource types in Topology graph and list view: T-03-TC04 + Given user is at Topology page + When user clicks on the Resources dropdown + And user checks Deployment Configs checkbox + And user unchecks other checkboxes if there are any + Then user will see the workloads of Deployment Configs resource types only + + + @regression @manual + Scenario: Ability to hide Deployment Config resource types in Topology graph and list view: T-03-TC05 + Given user is at Topology page + When user clicks on the Resources dropdown + And user checks all checkboxes + And user unchecks Deployment Configs checkbox + Then user will not see the workloads of Deployment Configs resource types only + + + @regression @manual + Scenario: Ability to show Virtual Machines resource types in Topology graph and list view: T-03-TC06 + Given user is at Topology page + When user clicks on the Resources dropdown + And user checks Virtual Machines checkbox + And user unchecks other checkboxes if there are any + Then user will see the workloads of Virtual Machines resource types only + + + @regression @manual + Scenario: Ability to hide Virtual Machines resource types in Topology graph and list view: T-03-TC07 + Given user is at Topology page + When user clicks on the Resources dropdown + And user checks all checkboxes + And user unchecks Virtual Machines checkbox + Then user will not see the workloads of Virtual Machines resource types only + + + @regression @manual + Scenario: Ability to show Event Sources resource types in Topology graph and list view: T-03-TC08 + Given user is at Topology page + When user clicks on the Resources dropdown + And user checks Event Sources checkbox + And user unchecks other checkboxes if there are any + Then user will see the workloads of Event Sources resource types only + + + @regression @manual + Scenario: Ability to hide Event Sources resource types in Topology graph and list view: T-03-TC09 + Given user is at Topology page + When user clicks on the Resources dropdown + And user checks all checkboxes + And user unchecks Event Sources checkbox + Then user will not see the workloads of Event Sources resource types only + + + @regression @manual + Scenario: Ability to show Helm Releases resource types in Topology graph and list view: T-03-TC10 + Given user is at Topology page + When user clicks on the Resources dropdown + And user checks Helm Releases checkbox + And user unchecks other checkboxes if there are any + Then user will see the workloads of Helm Releases resource types only + + + @regression @manual + Scenario: Ability to hide Helm Releases resource types in Topology graph and list view: T-03-TC11 + Given user is at Topology page + When user clicks on the Resources dropdown + And user checks all checkboxes + And user unchecks Helm Releases checkbox + Then user will not see the workloads of Helm Releases resource types only + + + @regression @manual + Scenario: Ability to show Knative Services resource types in Topology graph and list view: T-03-TC12 + Given user is at Topology page + When user clicks on the Resources dropdown + And user checks Knative Services checkbox + And user unchecks other checkboxes if there are any + Then user will see the Knative Services resource types only + + + @regression @manual + Scenario: Ability to hide Knative Services resource types in Topology graph and list view: T-03-TC13 + Given user is at Topology page + When user clicks on the Resources dropdown + And user checks all checkboxes + And user unchecks Knative Services checkbox + Then user will not see the Knative Services resource types only + + + @regression @manual + Scenario: Ability to show Stateful Set resource types in Topology graph and list view: T-03-TC14 + Given user is at Topology page + When user clicks on the Resources dropdown + And user checks Stateful Set checkbox + And user unchecks other checkboxes if there are any + Then user will see the workloads of Stateful Set resource types only + + + @regression @manual + Scenario: Ability to hide Stateful Set resource types in Topology graph and list view: T-03-TC15 + Given user is at Topology page + When user clicks on the Resources dropdown + And user checks all checkboxes + And user unchecks Stateful Set checkbox + Then user will not see the workloads of Stateful Set resource types only + diff --git a/integration-tests/features/topology/topology-add-operator-backed-service.feature b/integration-tests/features/topology/topology-add-operator-backed-service.feature new file mode 100644 index 0000000..7579fde --- /dev/null +++ b/integration-tests/features/topology/topology-add-operator-backed-service.feature @@ -0,0 +1,54 @@ +@topology +Feature: Create Operator backed service in topology page + As a user, I want to add operator backed service to existing workloads in topology + + + Background: + Given user has installed OpenShift Serverless Operator + And user has installed Red Hat OpenShift distributed tracing platform + And user has installed Service Binding Operator + And user has installed PostgreSQL Operator provided by Red Hat + And user is at developer perspective + And user has created or selected namespace "aut-topology-operator-backed" + And user is at Topology page + + + @regression @manual + Scenario: Create Operator Backed serivce using visual connector from existing workload: T-04-TC01 + Given user has created workload "hello-openshift" + When user drags connector from "hello-openshift" workload + And user drops visual connector on empty graph + And user clicks on Operator Backed option + And user searches for Jaeger + And user clicks on the Jaeger card + And user clicks on Create button on side bar + And user clicks on Create button on Create Jaeger page + And user clicks on Create button on Create Service Binding module + Then user will see visual connection between "hello-openshift" and Jaeger operator backed service + + + @regression @manual + Scenario: Create Operator Backed serivce using visual connector from existing knative service: T-04-TC02 + Given user has created knative service "knative demo" with revision present + When user drags connector from "knative-demo" workload + And user drops visual connector on empty graph + And user clicks on Operator Backed option + And user searches for Jaeger + And user clicks on the Jaeger card + And user clicks on Create button on side bar + And user clicks on Create button on Create Jaeger page + And user clicks on Create button on Create Service Binding module + Then user will see visual connection between "knative-demo" and Jaeger operator backed service + + + @regression @manual + Scenario: Create Operator Backed serivce using binding connector from workload: T-04-TC03 + Given user has created "nodejs-app" workload + When user drags connector from "nodejs-app" workload + And user drops visual connector on empty graph + And user clicks on Operator Backed option + And user searches for PostgreSQL + And user clicks on the PostgreSQL provided by Red Hat card + And user clicks on Create button on side bar + And user clicks on Create button on Create Database page + Then user will see binding connection between "nodejs-app" and PostgreSQL operator backed service diff --git a/integration-tests/features/topology/topology-application-grouping.feature b/integration-tests/features/topology/topology-application-grouping.feature new file mode 100644 index 0000000..0e91b9b --- /dev/null +++ b/integration-tests/features/topology/topology-application-grouping.feature @@ -0,0 +1,54 @@ +@topology +Feature: Application groupings in topology + As a user, I want to check application groupings + + Background: + Given user is at developer perspective + And user has created or selected namespace "aut-groupings-topology" + + + @smoke + Scenario: Verify Application grouping sidebar: T-05-TC01 + Given user is at Add page + And user has created workload "nodejs-ex-git" with resource type "Deployment" and application groupings "nodejs-ex-git-app" + When user clicks on application groupings "nodejs-ex-git-app" + Then user can see sidebar opens with Resources tab selected by default for application groupings + And user is able to see workload "nodejs-ex-git" under resources tab in the sidebar + And user can see Actions dropdown menu + + + @smoke + Scenario: Verify Application grouping context menu: T-05-TC02 + Given user is at Topology page + When user right clicks on application "nodejs-ex-git-app" to open Context Menu + Then user can view Add to application and Delete application options + + + @regression + Scenario: Add to Application in Application grouping from Action menu: T-05-TC03 + Given user is at Topology page + When user clicks on application groupings "nodejs-ex-git-app" + And user clicks on Action menu + And user hovers on Add to Application from action menu + And user clicks on Import From Git option + And user fills the form with workload name "added-application-1" and clicks Create + Then user can see "added-application-1" workload + + @regression + Scenario: Delete application grouping from Action menu: T-05-TC04 + Given user is at Add page + And user has created workload "nodejs-1" with resource type "Deployment" and application groupings "app2" + When user right clicks on application "app2" to open Context Menu + And user clicks on "Delete application" from context action menu + And user enters the name "app2" in the Delete application modal and clicks on Delete button + Then user will not see Application groupings "app2" + + @smoke + Scenario: Verify Application grouping in Topology List view: T-05-TC05 + Given user has created workload "ex-node-js" in application grouping "nodejs-ex-git-app" + And user is at Topology page + When user clicks on List view button + And user clicks on application grouping "nodejs-ex-git-app" in the list view + Then user can see sidebar opens with Resources tab selected by default for application groupings + And user is able to see workload "ex-node-js" under resources tab in the sidebar + And user can see Actions dropdown menu diff --git a/integration-tests/features/topology/topology-chart-area-visual.feature b/integration-tests/features/topology/topology-chart-area-visual.feature new file mode 100644 index 0000000..6e20db0 --- /dev/null +++ b/integration-tests/features/topology/topology-chart-area-visual.feature @@ -0,0 +1,506 @@ +@topology +Feature: Topology chart area + As a user, I want to verify topology chart visuals + + Background: + Given user is at developer perspective + And user has created or selected namespace "aut-topology-delete-workload" + + + @smoke + Scenario: Empty state of topology: T-06-TC01 + When user navigates to Topology page + Then user sees Topology page with message "No resources found" + And user is able to see Start building your application, Add page links + And Display options dropdown, Filter by resource and Find by name fields are disabled + And switch view is disabled + + + @regression + Scenario: Navigate to Add page from Empty state of topology: T-06-TC02 + Given user is at the Topology page + When user clicks on "Add page" link in the topology page + Then user will be redirected to Add page + + + @regression + Scenario: Add to project option in Empty state of topology: T-06-TC03 + Given user is at the Topology page + When user clicks on "Start building your application" link in the empty topology page + Then user will be able to see Add to project search bar + + + @smoke + Scenario: Topology with workloads: T-06-TC04 + Given user has created a deployment workload named "nodejs-ex-git-1" + And user has created a deployment config workload "nodejs-ex-git-2" + When user navigates to Topology page + Then user sees "nodejs-ex-git-1" and "nodejs-ex-git-2" workloads in topology chart area + + + @regression @manual + Scenario: Visual for deployment: T-06-TC05 + Given user has created a deployment workload named "nodejs-d" + And user is at the Topology page + When user checks nodes and the decorators associated with them + Then nodes are circular shaped with builder image in them + And pod ring associated with node are present around node with color according to the pod status + And deployment can have application url on top-right of the node + And user sees edit source code decorator is on bottom right of the node which can lead to github or che workspace + And user sees build decorator on bottom left which will take user to either build tab or pipeline depending on pipeline associated with them + And user checks node label having "D" for deployment and then name of node + + + @regression @manual + Scenario: Visual for deployment-config: T-06-TC06 + Given user has created a deployment config workload named "nodejs-dc" + And user is at the Topology page + When user checks nodes and the decorators associated with them + Then nodes are circular shaped with builder image in them + And pod ring associated with node are present around node with color according to the pod status + And deployment-config can have application url on top-right of the node + And user sees edit source code decorator is on bottom right of the node which can lead to github or che workspace + And user sees build decorator on bottom left which will take user to either build tab or pipeline depending on pipeline associated with them + And user checks node label having "DC" for deployment-config and then name of node + + + @regression @manual + Scenario: Visual for knative service with no revision: T-06-TC07 + Given user has created a knative service workload named "nodejs-knative" without revision + And user is at the Topology page + When user checks nodes and the decorators associated with them + Then user can view knative service are rectangular shaped with round corners + And user can see dotted boundary with text "No Revisions" mentioned + And knative sevice app can have application url on top-right of the node + And user sees build decorator on bottom left on knative service app which will take user to build tab + And user checks knative service having label "KSVC" and then the name of service + + + @regression @manual + Scenario: Visual for knative service with revisions: T-06-TC08 + Given user has created a knative service workload named "nodejs-knative" with revision + And user is at the Topology page + When user checks nodes and the decorators associated with them + Then user can view knative service are rectangular shaped with round corners + And user can see knative service app with dotted boundary with revision present inside it + And knative sevice app can have application url on top-right of the node + And user can see traffic distribution from knative sevice app to its revisions with its percentage number + And pod ring associated with revisions are present around node with color according to the pod status + And user sees edit source code decorator is on bottom right of knative service which can lead to github or che workspace + And user sees build decorator on bottom left on knative service app which will take user to either build tab + And user checks revisions having label "REV" and then the name + And user checks knative service having label "KSVC" and then the name of service + + + @smoke + Scenario: Context menu of node: T-06-TC09 + Given user has created a deployment workload named "nodejs-ex-git3" + And user is at the Topology page + When user right clicks on the node "nodejs-ex-git3" to open context menu + Then user is able to see context menu options like Edit Application Grouping, Edit Pod Count, Pause Rollouts, Add Health Checks, Add Horizontal Pod Autoscaler, Add Storage, Edit Update Strategy, Edit Labels, Edit Annotations, Edit Deployment, Delete Deployment + + + @regression @odc-4944 @manual + Scenario: Zoom In to 50% in topology: T-06-TC10 + Given user has created a workload named "nodejs-ex-git" + And user is at the Topology page + And chart view is totally zoomed out + When user clicks on Zoom In option to zoom to 50% scale + Then user can see the chart area is zoomed + And user can see all labels & decorators are hidden + And label are shown when hovering over the node + + + @regression @odc-4944 @manual + Scenario: Zoom In to 30% in topology: T-06-TC11 + Given user has created a workload named "nodejs-ex-git" + And user is at the Topology page + And chart view is totally zoomed out + When user clicks on Zoom In option to zoom to 30% scale + Then user can see the chart area is zoomed + And user can see all labels, decorators, pod rings & icons are hidden + And user can see background of node as white + + + @regression @manual + Scenario: Zoom Out in topology: T-06-TC12 + Given user has created a workload named "nodejs-ex-git" + And user is at the Topology page + When user clicks on Zoom Out option + Then user sees the chart area is zoomed out + + + @regression @manual + Scenario: Fit to Screen in topology: T-06-TC13 + Given user has created a workload named "nodejs-ex-git" + And user is at the Topology page + When user clicks on Zoom In option + And user sees the chart area is zoomed + And user clicks on Fit to Screen option + Then user sees the nodes fitting within chart area + + + @regression @manual + Scenario: Reset view in topology: T-06-TC14 + Given user has created a workload named "nodejs-ex-git" + And user is at the Topology page + When user clicks on Zoom In option + And user sees the chart area is zoomed + And user clicks on Reset View option + Then user sees the chart area is reset to original + + + @regression + Scenario Outline: Topology filter by resource: T-06-TC15 + Given user created "" workload + When user is at Topology page chart view + And user clicks the filter by resource on top + And user clicks on "" option + Then user can see only the "" workload + + Examples: + | resource_type | + | Deployment | + | DeploymentConfig | + + + @regression + Scenario: Context menu on empty area: T-06-TC16 + Given user has installed OpenShift Serverless Operator + And user has installed Crunchy Postgres for Kubernetes operator + And user navigates to Topology Page + When user right clicks on the empty chart area + And user hovers on Add to Project + Then user is able to see options like Samples, Import from Git, Container Image, From Dockerfile, From Devfile, From Catalog, Database, Operator Backed, Helm Charts, Event Source, Channel + + + @regression + Scenario: Add to Project in topology: T-06-TC17 + Given user is at the Topology page + When user right clicks on the empty chart area + And user hovers on Add to Project + And user clicks on Samples + And user selects go sample + And user fills the "Go Sample" form and clicks Create + And user hovers on Add to Project and clicks on "Import from Git" + And user fills the "Import From Git" form and clicks Create + And user hovers on Add to Project and clicks on "Container Image" + And user fills the "Container Image" form and clicks Create + And user hovers on Add to Project and clicks on "From Catalog" + And user selects Python Builder Image and clicks Create Application + And user fills the "Catalog" form and clicks Create + And user hovers on Add to Project and clicks on "Database" + And user selects Postgres Database and clicks on Instantiate Template + And user clicks on Create button + # Crunchy Postgres for Kubernetes operator not installing correctly, won't able to create a postgres. + And user hovers on Add to Project and clicks on "Operator Backed" + And user selects Redis and clicks on Create + And user fills the "Operator Backed" form with yaml at "test-data/redis-standalone.yaml" and clicks Create + And user hovers on Add to Project and clicks on "Helm Charts" + And user selects Nodejs and clicks on Install Helm Charts + And user fills the "Helm Chart" form and clicks Create + And user hovers on Add to Project and clicks on "Event Source" + And user selects Api Server Source and clicks on Create Event Source + And user fills the "Event Source" form and clicks Create + And user hovers on Add to Project and clicks on "Channel" + And user fills the "Channel" form and clicks Create + Then user is able to see different applications created from Samples, Import from Git, Container Image, From Catalog, Database, Operator Backed, Helm Charts, Event Source, Channel + + + @regression @manual + Scenario: Upload JAR file form: T-06-TC18 + Given user has a jar file named "sample_yaml_upload.yaml" + And user is at the Topology page + When user drags and drop jar file on topology + Then user sees Upload JAR file form + And user can see JAR section with jar file name with Browse and Clear button associated with it + And user can see Optional java commands, Runtime icon and Build image version under JAR section + And user can see General section with Application name and Name under it + And user can see Advanced options section + + + @regression @manual + Scenario: Drag and drop jar file in topology chart view: T-06-TC19 + Given user has a jar file named "sample_yaml_upload.yaml" + And user is at the Topology page + When user drags and drop jar file on topology + And user gives Application name as "sample-upload-app" and workload Name as "sample-yaml-upload" + And user clicks on Create in Upload JAR file form + Then user is redirected to topology + And user can see a toast notification of JAR file uploading with link to build logs + And user can see deployment "sample-yaml-upload" in application "sample-upload-app" is created in topology + + + @regression @manual + Scenario: Add to Project to upload JAR file in topology: T-06-TC20 + Given user is at the Topology page + When user right clicks on the empty chart area + And user clicks on Add to Project + And user clicks on Upload JAR file + And user clicks on Browse in JAR file section + And user selects file to upload + And user clicks Clear and reupload the file + And user gives Application name as "sample-upload-app" and workload Name as "sample-yaml-upload-1" + And user clicks on Create + Then user is redirected to topology + And user can see a toast notification of JAR file uploading with link to build logs + And user can see deployment "sample-yaml-upload-1" in application "sample-upload-app" is created in topology + + + @regression @manual + Scenario: Add to Project through drag and drop to upload JAR file in topology: T-06-TC21 + Given user is at the Topology page + When user right clicks on the empty chart area + And user clicks on Add to Project + And user clicks on Upload JAR file + And user drag and drop the file in JAR file section + And user gives Application name as "sample-upload-app" and workload Name as "sample-yaml-upload-1" + And user clicks on Create + Then user is redirected to topology + And user can see a toast notification of JAR file uploading with link to build logs + And user can see deployment "sample-yaml-upload-1" in application "sample-upload-app" is created in topology + + + @regression @manual + Scenario: Drag and drop Incompatible file in topology chart view: T-06-TC22 + Given user has a incompatible file + And user is at the Topology chart view + When user drags and drops the file on topology + Then a toast warning message will appear stating that the file is invalid. + + + @regression @manual + Scenario: Exiting the browser while an upload is in progress: T-06-TC23 + Given user is uploading a jar file + And user is at the Topology chart view + When user tries to exist the browser + Then a web alert would appear asking the user if they really wanted to leave the page with Leave and Skip button + + + @regression @manual + Scenario: View shortcuts menu: T-06-TC24 + Given user has uploaded a jar file + And user is at Topology page + When user clicks on View shortcuts + Then user sees shortcut for Move + And user sees shortcut for Edit Application grouping + And user sees shortcut for Access context menu + And user sees shortcut for View details in side panel + And user sees shortcut for Access create connector handle + And user sees shortcut for Qpen quick search modal + And user sees shortcut for Drag and drop a JAR file into Topology + + + @regression @manual + Scenario: Display of External Bindable resources: T-06-TC25 + Given user has installed Service Binding operator + #Please refer to test case KM-01-TC01 for creating kafka connection + And user has created external bindable resource Kafka Connection "kafka-instance-123" + When user navigates to Topology chart view + Then user will see the bindable resource "kafka-instance-123" in trapezoid shape + + + @regression @manual + Scenario: Connect to External Bindable resources: T-06-TC26 + Given user has installed Service Binding operator + #Please refer to test case KM-01-TC01 for creating kafka connection + And user has created external bindable resource Kafka Connection "kafka-instance-123" + And user is at the Topology chart view + When user created a deployment workload "node-js-git-1" + And user drag the connector from the deployment workload + And user drops the connector on the enabled bindable resource + Then user will see service binding connection + + + @regression @odc-6361 + Scenario: Search with label: T-06-TC27 + Given user has created a deployment workload "nodejs-1" + And user has created a deployment workload "nodejs-2" + And user is at Topology page chart view + When user selects "Label" option in filter menu + And user searches for label "app.kubernetes.io/component=nodejs-1" + Then user can see the workload "nodejs-1" visible + + + @regression @odc-6361 + Scenario: Check last selected node in topology per project per session: T-06-TC28 + Given user has created a deployment workload "nodejs-1" + And user has created a deployment workload "nodejs-2" + And user is at Topology page chart view + When user clicks on workload "nodejs-2" to open sidebar + And user opens the details page for "nodejs-2" by clicking on the title + And user navigate back to Topology page + Then user will see the the workload "nodejs-2" selected with sidebar open + + + @regression @odc-5947 + Scenario: Create Service Binding option in nodes actions menu: T-06-TC29 + Given user has installed Service Binding operator + And user has created or selected namespace "binding-service" + And user is at developer perspective + And user is at Topology page chart view + And user has created a deployment workload "node-js1" + When user right clicks on workload "node-js1" + And user clicks on "Create Service Binding" option from context menu + Then user will see "Create Service Binding" modal + And user will see alert "No bindable services available" + + + @regression @manual @odc-5947 + Scenario: Bindable services options in Create Service Binding modal: T-06-TC30 + Given user has installed Service Binding operator + And user is at Topology page chart view + And user has created a deployment workload "node-js2" + #Please refer to test case KM-01-TC01 for creating kafka connection + And user has created external bindable resource Kafka Connection "kafka-instance-ex" + And user has created operator-backed service of postgresSQL "example-pg" + And user has applied '/testdata/bindableresource1.yaml' yaml + When user right clicks on workload "node-js2" + And user clicks on "Create Service Binding" option from context menu + And user clicks on Bindable service dropdown + Then user will see postgres and kafka connection services options + And user is able to see service binding connector with name "node-js2-d-kafka-example-pg-pc" after clicking on create with "example-pg" option selected in Create Service Binding modal + + + @regression @manual @odc-5947 + Scenario: Drag and drop connector to existing bindable resource: T-06-TC31 + Given user has installed Service Binding operator + And user is at Topology page chart view + And user has created a deployment workload "node-s" + #Please refer to test case KM-01-TC01 for creating kafka connection + And user has created external bindable resource Kafka Connection "kafka-instance-ex" + When user drag and drop the connector to Kafka Connection + And user clicks on Create with name "node-s-d-kafka-instance-ex-akc" + And user clicks on connector + Then user will see the name as "node-s-d-kafka-instance-ex-akc" + And user will see Secret section with secret present + + + @regression @manual @odc-5947 + Scenario: Specify the name and the bindable object to connect to in Create Service Binding modal: T-06-TC32 + Given user has installed Service Binding operator + And user is at Topology page chart view + And user has created a deployment workload "node-js" + When user drag and drop the connector to empty area + And user selects Operator backed option + And user selects Kafka Connection + And user clicks on Create + And user clicks on Create button on Create Kafka Connection form + And user replaced the name "node-js-d-kafka-instance-ex-akc" with "node-kc-connection-1" in Create Service Binding modal + And user clicks on Create + Then user will see the connection between node workload and Kafka Connection + And user will see the name "node-kc-connection-1" in connector sidebar + + + @regression @manual @odc-5947 + Scenario: Create connection to already existing service binding connection: T-06-TC33 + Given user has installed Service Binding operator + And user is at Topology page chart view + #Please refer to test case KM-01-TC01 for creating kafka connection + And user has created service binding connnector between deployment workload "node-j" and Kafka Connection "kafka-instance-ex1" + When user right clicks on workload "node-j" + And user clicks on "Create Service Binding" option from context menu + And user selects Bindable service as "kafka-instance-ex1" + And user clicks on Save + Then user will see error "Service binding already exists. Select a different service to connect to." + + + @regression @odc-4944 @manual + Scenario: Status on Service binding in topology: T-06-TC34 + Given user has installed Service Binding operator + And user has installed Redis Operator + And user has installed Crunchy Postgres for Kubernetes operator + And user is at developer perspective + And user is at Topology page chart view + And user has created a deployment workload named "node-j" + And user has created a operator backed service of "Redis" operator named "redis-standalone" + And user has created a operator backed service "hippo" from yaml "test-data/hippo-postgres-cluster.yaml" + And user has created service binding connnector "test-connector1" between "node-j" and "redis-standalone" + And user has created service binding connnector "test-connector2" between "node-j" and "hippo" + When user clicks on service binding connector for "hippo" + And user clicks on service binding connector for "redis-standalone" + Then user will see black colored connector for "hippo" + And user can see "Connected" in Status section on service binding connnector topology sidebar + And user will see red colored connector for "redis-standalone" + And user can see "Error" in Status section on service binding connnector topology sidebar + + + @regression @odc-4944 + Scenario: Connected status on Service binding details page: T-06-TC35 + Given user has created namespace "aut-connected-sb" + And user has installed Service Binding operator + And user has installed Redis Operator + And user is at developer perspective + And user is at Topology page chart view + And user has created a deployment workload named "node-ej" + And user has created a operator backed service of "Redis" operator named "redis-standalone" + And user has created service binding connnector "test-connector2" between "node-ej" and "redis-standalone" + When user clicks on service binding connector + And user clicks on the service binding name "test-connector2" at the sidebar + Then user will see "Connected" Status on Service binding details page + + + @regression @odc-4944 + Scenario: Error status on Service binding details page: T-06-TC36 + Given user has created namespace "aut-error-sb" + And user has installed Service Binding operator + And user has installed Redis Operator + And user is at developer perspective + And user is at Topology page chart view + And user has created a deployment workload named "node-ej" + And user has created a operator backed service "redis-standalone" from yaml "test-data/redis-standalone.yaml" + And user has created service binding connnector "test-connector3" between "node-ej" and "redis-standalone" + When user clicks on service binding connector + And user clicks on the service binding name "test-connector3" at the sidebar + Then user will see "Error" Status on Service binding details page + + + @regression @odc-7120 + Scenario: Create connection using import YAML with Service Binding using Label Selector: T-06-TC37 + Given user has created namespace "aut-connected-sb-ls" + And user has installed Service Binding operator + And user has installed Redis Operator + And user is at developer perspective + And user is at Topology page chart view + And user has created a deployment workload named "node-ej" + And user has created a operator backed service of "Redis" operator named "redis-standalone" + When user clicks on import YAML button from topology page + And user enters yaml content from yaml file "test-data/servicebinding-resource-label-selector.yaml" in the editor + And user clicks on Create button in import YAML + And user sees "test-connector4" Title on Service binding details page + And user navigates to Topology page + Then user will see service binding connection + + + @regression @odc-7120 + Scenario: Label specified in Label Selector section on Service binding details page: T-06-TC38 + Given user has created namespace "aut-label-details-sb" + And user has installed Service Binding operator + And user has installed Redis Operator + And user is at developer perspective + And user is at Topology page chart view + And user has created a deployment workload named "node-ej" + And user has created a operator backed service of "Redis" operator named "redis-standalone" + When user clicks on import YAML button from topology page + And user enters yaml content from yaml file "test-data/servicebinding-resource-label-selector.yaml" in the editor + And user clicks on Create button in import YAML + And user sees "test-connector4" Title on Service binding details page + Then user will see "app=node-ej" in Label Selector section on Service binding details page + + + @regression @odc-7120 + Scenario: Label specified in Label Selector section on Service binding side panel: T-06-TC39 + Given user has created namespace "aut-label-panel-sb" + And user has installed Service Binding operator + And user has installed Redis Operator + And user is at developer perspective + And user is at Topology page chart view + And user has created a deployment workload named "node-ej" + And user has created a operator backed service of "Redis" operator named "redis-standalone" + When user clicks on import YAML button from topology page + And user enters yaml content from yaml file "test-data/servicebinding-resource-label-selector.yaml" in the editor + And user clicks on Create button in import YAML + And user sees "test-connector4" Title on Service binding details page + And user navigates to Topology page + And user clicks on service binding connector + Then user will see "app=node-ej" in Label Selector section on Service binding connnector topology sidebar diff --git a/integration-tests/features/topology/topology-connecting-workloads.feature b/integration-tests/features/topology/topology-connecting-workloads.feature new file mode 100644 index 0000000..561a979 --- /dev/null +++ b/integration-tests/features/topology/topology-connecting-workloads.feature @@ -0,0 +1,29 @@ +@topology +Feature: Connecting nodes + As a user, I want to connect two application + + Background: + Given user is at developer perspective + And user is at Add page + And user has created or selected namespace "aut-tp-connect-workloads" + And user has created workload "nodejs-ex-git" with resource type "Deployment" + And user is at Add page + + + @smoke + Scenario: Create visual connection between two nodes using Annotations: T-07-TC01 + Given user has created workload "dancer-ex-git" with resource type "Deployment Config" + When user clicks on workload "nodejs-ex-git" + And user clicks on Action menu + And user clicks "Edit annotations" from action menu + And user enters key as "app.openshift.io/connects-to" + And user enters value as "dancer-ex-git" to which it will be connected + Then user can see that two workloads are connected with arrow + + + @regression @manual + Scenario: Create visual connection between two nodes using drag and drop: T-07-TC02 + Given user has created two worloads "nodejs-ex-git" and "dancer-ex-git" + When user scrolls over a node to see the arrow + And user click on the front of arrow and drag it on to the other node and drop it + Then user can see the arrow connecting them with head pointing to the node where the arrow is dropped diff --git a/integration-tests/features/topology/topology-display-filter.feature b/integration-tests/features/topology/topology-display-filter.feature new file mode 100644 index 0000000..b4e703a --- /dev/null +++ b/integration-tests/features/topology/topology-display-filter.feature @@ -0,0 +1,58 @@ +@topology +Feature: Topology Display Filter Group + As a user, I should be able to use the Display Filter Groups on Topology page + + Background: + Given user is at developer perspective + And user has created or selected namespace "aut-display-filter" + And user is at Add page + + + @regression + Scenario: Topology display filter by expand option: T-18-TC01 + Given user has created workload "nodejs-ex-git" with resource type "Deployment" + And user is at Topology page + And user is at Topology Graph view + When user clicks display options + And user disbales expand option + Then user will see the application_groupings checkbox will disable + And user will see workload in text view + + + @regression + Scenario: Topology display filter by application grouping option: T-18-TC02 + Given user is at Topology page + And user is at Topology Graph view + When user clicks display options + And user enables expand option + And user unchecks the application grouping option + Then user will see workload in text view + + + @regression + Scenario: Topology display filter by pod count option: T-18-TC03 + Given user is at Topology page + And user is at Topology Graph view + When user clicks display options + And user checks the application grouping option + And user checks the pod count option + Then user will able to see the pod count inside workload + + + @regression + Scenario: Topology display filter by labels option: T-18-TC04 + Given user is at Topology page + And user is at Topology Graph view + When user clicks display options + And user unchecks the labels option + Then user will not able to see the labels on workload + + @regression + Scenario: Topology display filter by expand option in list view: T-18-TC05 + Given user is at Topology page + And user is at topology list view + When user clicks display options + And user disbales expand option + Then user will see the application_groupings checkbox will disable + And user will see deployment section is not visible + And user will see deployments in count view \ No newline at end of file diff --git a/integration-tests/features/topology/topology-editing-app-node.feature b/integration-tests/features/topology/topology-editing-app-node.feature new file mode 100644 index 0000000..ce72aa2 --- /dev/null +++ b/integration-tests/features/topology/topology-editing-app-node.feature @@ -0,0 +1,153 @@ +@topology +Feature: Editing an application + As a user, I want to edit an application + + Background: + Given user is at developer perspective + And user has created or selected namespace "aut-topology-edit-node-resource" + And user is at Add page + + + @smoke + Scenario Outline: Editing a workload: T-09-TC01 + Given user has created workload "" with resource type "" + When user right clicks on the workload "" to open the Context Menu + And user clicks on "Edit " from context action menu + And user edits application groupings to "" + And user saves the changes + Then user can see application groupings updated to "" + + Examples: + | resource_type | workload_name | application_groupings | + | Deployment | nodejs-ex-git | app1 | + | Deployment Config | dancer-ex-git-1 | app2 | + + + @smoke + Scenario: Editing a knative service: T-09-TC02 + Given user has installed OpenShift Serverless Operator + And user is at Add page + And user has created or selected namespace "edit-knative-service" + And user has created workload "nodejs-ex-git-kn" with resource type "Knative Service" + And user clicks on knative workload "nodejs-ex-git-kn" to verify that build is completed + When user right clicks on the knative workload "nodejs-ex-git-kn" to open the Context Menu + And user clicks on "Edit nodejs-ex-git-kn" from context action menu + And user edits application groupings to "new-app" + And user saves the changes + Then user can see application groupings updated to "new-app" + + + @regression + Scenario: Advanced image options in Edit deployment/deployment config form: T-09-TC03 + Given user has created workload "nodejs-advanced" with resource type "Deployment" + When user right clicks on the workload "nodejs-advanced" to open the Context Menu + And user clicks "Edit Deployment" from action menu + And user clicks on Show advanced image options + And user clicks on Create new secret + And user creates a new secret "new-secret" + Then user will see "new-secret" in secret name dropdown under Pull secret + + + @smoke + Scenario: Editing deployment resource limits through form view: T-09-TC04 + Given user has created workload "resource-limit" with resource type "Deployment Config" + When user right clicks on the workload "resource-limit" to open the Context Menu + And user clicks "Edit resource limits" from action menu + And user enters value of CPU Request as "250" + And user enters value of CPU Limit as "500" + And user enters value of Memory Request as "64" + And user enters value of Memory Limit as "128" + And user clicks on Save + Then user will be redirected to Topology with the updated resource limits + + + @smoke @broken-test + Scenario: Editing deployment using form view: T-09-TC05 + Given user has created workload "rolling-update" with resource type "Deployment" + When user right clicks on the workload "rolling-update" to open the Context Menu + And user clicks "Edit Deployment" from action menu + And user selects "Rolling Update" Strategy type under Deployment Strategy + And user enters value of Maximum number of unavailable Pods and Maximum number of surge Pods as "25%" + #2091102 : https://bugzilla.redhat.com/show_bug.cgi?id=2091102 + And user selects value of project, image stream and tag section under images as "openshift", "golang" and "latest" respectively + And user enters NAME as "DEMO_GREETING" and VALUE as "Hello from the environment" + And user clicks on Show advanced image options + And user clicks on Create new secret + And user creates a new secret "new-secret1" + And user selects secret "new-secret1" from Pull secret dropdown + And user selects the Pause rollouts check box under advanced options section + And user saves the changes + Then user will be redirected to Topology with the updated deployment + + + @regression + Scenario Outline: Editing deployment config using form view: T-09-TC06 + Given user has created workload "recreate-update" with resource type "Deployment Config" + When user right clicks on the workload "recreate-update" to open the Context Menu + And user clicks "Edit DeploymentConfig" from action menu + And user selects "Recreate" Strategy type under Deployment Strategy + And user enters Timeout value as "600" + And user clicks on Show additional parameters and lifcycle hooks + And user adds Pre Cycle Hook for workload "recreate-update" with failure policy "Abort" + And user adds Mid Cycle Hook for workload "recreate-update" with failure policy "Retry" + And user adds Post Cycle Hook for workload "recreate-update" with failure policy "Ignore" + And user unchecks Deploy image from an image stream tag checkbox + And user enters value of Image name as "" + And user saves the changes + Then user will be redirected to Topology with the updated deployment + + Examples: + | image_value | + | quay.io/openshift-release-dev/ocp-v4.0-art-dev@sha256:51435011b4f381e292cd70c231d45a35add8b2d28ccac707c52802c143604630 | + + + @regression @manual + Scenario: Edit JAR file through drag and drop: T-09-TC07 + Given user has uploaded JAR file named "sample-jar" + When user opens sidebar of the JAR workload + And user clicks on Edit "sample-jar" in Action menu + And user drag and drop a new JAR file in JAR file section + And user updates Build image version + And user clicks on Save + Then user is redirected to topology + And user can see a toast notification of JAR file uploading with link to build logs + + + @regression @manual + Scenario: Additional parameters and lifecycle hooks for Edit deployment config: T-09-TC08 + Given user is on Edit DeploymentConfig page + When user changes Strategy type to "Recreate" + And user clicks on "Show additional parameters and lifecycle hooks" option + And user clicks on "Add Pre Cycle Hook" under Pre Cycle Hook section + And user clicks on "Add Mid Cycle Hook" under Mid Cycle Hook section + And user clicks on "Add Post Cycle Hook" under Post Cycle Hook section + Then user will see "Lifecycle action", "Container name", "Command", "Environment variables(runtime only)" "Volumes" and "Failure policy" sections in each of the form + And user will see Tick and cross buttons associated with the forms + + @regression @manual + Scenario Outline: Advanced container options in Edit deployment/deployment config form: T-09-TC09 + Given user has "" workload "workload-d" + And user is on Edit "" page + When user selects "Deploy image from an image stream tag" checkbox + And user clicks on "Show advanced image options" option + Then user can see Pull Secret dropdown and Create new secret option + + Examples: + | resource | + | Deployment | + | DeploymentConfig | + + + @regression @manual + Scenario Outline: Additional advanced sections in Edit deployment/deployment config form: T-09-TC10 + Given user has "" workload "workload-d" + And user is on Edit "" page + When user clicks on "Pause rollouts" in advanced options + And user clicks on "Scaling" option + Then user will see "Pause rollouts for this " checkbox under Pause rollouts section + And user will see "Replicas" in Scaling section + + Examples: + | resource | value | + | Deployment | deployment | + | DeploymentConfig | deployment config | diff --git a/integration-tests/features/topology/topology-filter-bar-display.feature b/integration-tests/features/topology/topology-filter-bar-display.feature new file mode 100644 index 0000000..f557b36 --- /dev/null +++ b/integration-tests/features/topology/topology-filter-bar-display.feature @@ -0,0 +1,69 @@ +@topology +Feature: Workload Groupings in Topology + User will be able to expand and collapse all groups on Topology graph and list view + + + Background: + Given user is at developer perspective + And user has created or selected namespace "aut-topology-display-options" + And user is at Add page + + + @smoke + Scenario: Default state of Display dropdown: T-08-TC01 + Given user has created workload "nodejs-ex-git" with resource type "Deployment" + When user clicks on the Display dropdown + Then user will see the Expand is checked + And user will see the Pod count is unchecked + + + @regression + Scenario: Uncheck the Expand: T-08-TC02 + Given user is at Topology page + When user clicks on the Display dropdown + And user unchecks the Expand + Then user will see that the Expand options are disabled + + + @regression @manual + Scenario: Provide ability to hide and show Helm release groupings in Topology graph and list view: T-08-TC03 + Given user is at the Topology page + And user is at the graph view + When user clicks on the Display dropdown + And user unchecks the Helm Release checkbox + Then user will see the Helm releases collapsed + And user will see the summary of workloads + + + @regression @manual + Scenario: Provide ability to hide and show Knative Services groupings in Topology graph and list view: T-08-TC04 + Given user is at the Topology page + And user is at the graph view + When user clicks on the Display dropdown + And user unchecks the Knative Services checkbox + Then user will see the Knative Services collapsed + And user will see the summary of workloads + + + @regression @manual + Scenario: Provide ability to hide and show Operator Groups groupings in Topology graph and list view: T-08-TC05 + Given user is at the Topology page + And user is at the graph view + When user clicks on the Display dropdown + And user unchecks the Operator Groups checkbox + Then user will see the Operator Groups collapsed + And user will see the summary of workloads + + + @regression @manual + Scenario: Display options menu in topology with defaut options: T-08-TC06 + Given user has created deployment, deployment-config and knative-service resource type git workloads + When user clicks on Display Options + And user sees "Pod Count" and "Labels" under "Show" and "Expand" have options according to their presence which are "Application Groupings" and "knative Services" + And user deselects "Labels" which is selected by default + And user sees the labels under the workloads have disappeared + And user hovers over application grouping the label appears + And user selects "Pod Count" which is deselected by default + And user checks the workloads which shows pod count instead of buider images + And user deselects "Application Groupings" in the Expand section + Then user can see workloads squashed in Application grouping diff --git a/integration-tests/features/topology/topology-in-context-add-options.feature b/integration-tests/features/topology/topology-in-context-add-options.feature new file mode 100644 index 0000000..2230679 --- /dev/null +++ b/integration-tests/features/topology/topology-in-context-add-options.feature @@ -0,0 +1,33 @@ +@topology +Feature: Add in context from the Developer Catalog + As a user, I want to add things in the context in topology + + + Background: + Given user has installed OpenShift Serverless Operator + And user is at developer perspective + And user has created or selected namespace "aut-topology-in-context-add" + And user has created "knative-demo" workload in "aut-knative-demos" application + And user is at Topology page + + + @smoke + Scenario: Add to Project in Context options: T-10-TC01 + When user right clicks on empty graph view + And user hovers on Add to Project + Then user can see in context options "Samples", "Import from Git", "Container Image", "From Catalog", "Database", "Operator Backed", "Helm Charts", "Event Source", "Channel" + + + @regression + Scenario: Add to Application in Context: T-10-TC02 + When user right clicks on Application Grouping "aut-knative-demos" + And user hovers on Add to application + Then user can see in context options "Import from Git", "Container Image", "Event Source", "Channel" + + + @regression + Scenario: Delete application from the Context options: T-10-TC03 + When user right clicks on Application Grouping "aut-knative-demos" + And user clicks on Delete application + And user enters the name "aut-knative-demos" in the Delete application modal and clicks on Delete button + Then user won't be able to see the "aut-knative-demos" Application Groupings diff --git a/integration-tests/features/topology/topology-layout-save.feature b/integration-tests/features/topology/topology-layout-save.feature new file mode 100644 index 0000000..9541fc3 --- /dev/null +++ b/integration-tests/features/topology/topology-layout-save.feature @@ -0,0 +1,49 @@ +@topology +Feature: Topology Layout should be saved + As a user, working in the topology, the graphical layout should be remembered for each project + + + Background: + Given user is at the topology page + And user has selected namespace "aut-topology-layout-save" + And user has created workload "hello-openshift" + And user logs in with kubeadmin credentials + + + @manual + Scenario: Topology Graph View persistence: T-11-TC01 + Given user has selected Graph View + When user navigates to add page + And user navigates to topology page + Then user will see topology Graph view + + + @manual + Scenario: Topology List View persistence: T-11-TC02 + Given user has selected List View + When user logs out from cluster + And user logs in to cluster with kubeadmin credentials + Then user will see topology view unchanged + + + @manual + Scenario: Persistence Topology Layout across page views: T-11-TC03 + Given user has created workload "hello-openshift" + And user kept workload on right top corner + And user has zoomed in the topology to a certain amount + When user navigates to add page + And user navigates to topology page + Then user will see topology page unchanged + And user will see node location unchanged + And user will see zoom level unchanged + + + @manual + Scenario: Persistence Topology Layout after logging out from cluster: T-11-TC04 + Given user has created workload "hello-openshift" + And user kept workload on right top corner + When user logs out from cluster + And user logs in to cluster with kubeadmin credentials + And user navigates to topology page + Then user will see topology page unchanged + And user will see node location unchanged diff --git a/integration-tests/features/topology/topology-list-view.feature b/integration-tests/features/topology/topology-list-view.feature new file mode 100644 index 0000000..72ece68 --- /dev/null +++ b/integration-tests/features/topology/topology-list-view.feature @@ -0,0 +1,42 @@ +@topology +Feature: List view in topology + As a user, I want to see list view in topology + + Background: + Given user is at developer perspective + And user has created or selected namespace "aut-topology-list-view" + And user is at Add page + + + @smoke + Scenario: Topology List view: T-12-TC01 + Given user has created workload "nodejs-ex-git-d" with resource type "Deployment" + When user clicks on List view button + Then user will see workloads are segregated by applications groupings + + + @regression @manual + Scenario: Drag and drop jar file in topology list view: T-12-TC02 + Given user has a jar file named "sample_yaml_upload.yaml" + And user is at the Topology list view page + When user drags and drop jar file on topology + And user gives Application name as "sample-upload-app" and workload Name as "sample-yaml-upload" in Upload JAR file form + And user clicks on Create + Then user is redirected to topology + And user can see a toast notification of JAR file uploading with link to build logs + And user can see deployment "sample-yaml-upload" in application "sample-upload-app" is created in topology + + + @regression @manual + Scenario: Drag and drop Incompatible file in topology list view: T-12-TC03 + Given user has a incompatible file + And user is at the Topology list view + When user drags and drop the file on topology + Then the curser will show the action is not available + + + @regression @manual + Scenario: View shortcuts menu: T-12-TC04 + Given user has uploaded a jar file + When user clicks on View shortcuts in topology list view + Then user sees shortcut for Drag and drop a JAR file into Topology diff --git a/integration-tests/features/topology/topology-toolbar-filter.feature b/integration-tests/features/topology/topology-toolbar-filter.feature new file mode 100644 index 0000000..ec0fd4d --- /dev/null +++ b/integration-tests/features/topology/topology-toolbar-filter.feature @@ -0,0 +1,20 @@ +@topology +Feature: Topology Toolbar Filter Group + As a user, I should be able to use the Filter Groups on Topology page + + Background: + Given user is at developer perspective + And user has created or selected namespace "aut-tp-toolbar" + And user is at Add page + + + @regression + Scenario: Topology filter by resource: T-13-TC01 + Given user has created workload "nodejs-ex-git-d" with resource type "Deployment" + And user is at Add page + And user has created workload "nodejs-ex-git-dc" with resource type "Deployment Config" + When user clicks on List view button + And user clicks the filter by resource on top + Then user will see Deployment and DeploymentConfig options with 1 associated with it + And user clicks on Deployment checkbox to see only the deployment type workload + And user clicks on DeploymentConfig checkbox to see only the deploymentconfig type workload diff --git a/integration-tests/features/topology/topology-workload-sidebar.feature b/integration-tests/features/topology/topology-workload-sidebar.feature new file mode 100644 index 0000000..6028b31 --- /dev/null +++ b/integration-tests/features/topology/topology-workload-sidebar.feature @@ -0,0 +1,87 @@ +@topology +Feature: Sidebar in topology + As a user, I want to check sidebar of workloads + + Background: + Given user is at developer perspective + And user has created or selected namespace "aut-topology-sidebar" + And user is at Add page + + + @smoke + Scenario: Sidebar for workload: T-14-TC01 + Given user has created workload "nodejs-ex-git" with resource type "deployment" + When user clicks on workload "nodejs-ex-git" + Then user can see sidebar opens with Resources tab selected by default + And user can see sidebar Details, Resources and Observe tabs + And user verifies name of the node "nodejs-ex-git" and Action drop down present on top of the sidebar + And user is able to see health check notification + And user can see traffic details for pod + And user can see close button + + + @smoke + Scenario: Sidebar for knative service: T-14-TC02 + Given user has installed OpenShift Serverless Operator + And user is at developer perspective + And user has selected namespace "aut-topology-sidebar" + And user is at Add page + And user has created workload "hello-openshift" with resource type "Knative Service" + When user clicks on knative workload "hello-openshift" + Then user can see sidebar opens with Resources tab selected by default + And user can see sidebar Details, Resources tabs + And user verifies name of the node "hello-openshift" and Action drop down present on top of the sidebar + And user can see traffic details for pod + And user can see close button + + + @regression + Scenario: Pod scale up in sidebar: T-14-TC03 + Given user has created workload "nodejs-ex-git-1" with resource type "deployment" + When user clicks on workload "nodejs-ex-git-1" + And user goes to Details tab + And user can see pod count is 1 + And user scales up the pod + Then user is able to see pod Scaling to "2Pods" for workload "nodejs-ex-git-1" + + + @regression + Scenario: Pod scale down in sidebar: T-14-TC04 + Given user can see workload "nodejs-ex-git-1" is created + When user clicks on workload "nodejs-ex-git-1" + And user opens Details tab + And user scales down the pod number + Then user is able to see pod Scaling to "1Pod" for workload "nodejs-ex-git-1" + + + @regression @odc-6361 @manual + Scenario: Resize the workload sidebar: T-14-TC05 + Given user has created workload "nodejs-ex-git-1" with resource type "deployment" + When user clicks on workload "nodejs-ex-git-1" + And user drags the sidebar from the edge + Then user is able to resize the sidebar + + + @regression @odc-6361 + Scenario: Change the route url with annotation: T-14-TC06 + Given user has created a deployment workload "nodejs-ex-2" + And user is at Topology chart view + When user clicks on workload "nodejs-ex-2" to open sidebar + And user clicks on Action menu + And user clicks on "Edit annotations" from action menu + And user enters key as "app.openshift.io/route-url" + And user enters value as "https://openshift.com" + Then user can see the new route href in route decorator be "https://openshift.com" + + + @regression @odc-6361 + Scenario: Removing route through annotations: T-14-TC07 + Given user has created a deployment workload "nodejs-ex-3" + And user is at Topology chart view + When user clicks on workload "nodejs-ex-3" to open sidebar + And user clicks on Action menu + And user clicks on "Edit annotations" from action menu + And user deletes the existing annotation for route + And user enters key as "app.openshift.io/route-disabled" + And user enters value as "true" + Then user can see route decorator has been hidden for workload "nodejs-ex-3" diff --git a/integration-tests/features/topology/topoplogy-delete-workload.feature b/integration-tests/features/topology/topoplogy-delete-workload.feature new file mode 100644 index 0000000..14c3949 --- /dev/null +++ b/integration-tests/features/topology/topoplogy-delete-workload.feature @@ -0,0 +1,27 @@ +@topology +Feature: Deleteing an application node + As a user, I want to delete an application + + Background: + Given user is at developer perspective + And user has created or selected namespace "aut-topology-delete-workload" + And user is at Add page + + + @regression + Scenario: Deleting a workload through Action menu: T-15-TC01 + Given user has created workload "nodejs-ex-git-d" with resource type "Deployment" + When user clicks on workload "nodejs-ex-git-d" + And user clicks on Action menu + And user clicks "Delete Deployment" from action menu + And user clicks on Delete button from modal + Then user will see workload disappeared from topology + + + @regression + Scenario: Deleting a workload through context menu: T-15-TC02 + Given user has created workload "nodejs-ex-git-dc" with resource type "Deployment Config" + When user right clicks on the workload "nodejs-ex-git-dc" to open the Context Menu + And user clicks "Delete DeploymentConfig" from action menu + And user clicks on Delete button from modal + Then user will see workload disappeared from topology diff --git a/integration-tests/features/topology/workloads-on-topology.feature b/integration-tests/features/topology/workloads-on-topology.feature new file mode 100644 index 0000000..c870181 --- /dev/null +++ b/integration-tests/features/topology/workloads-on-topology.feature @@ -0,0 +1,27 @@ +@topology +Feature: Topology + User will be able to create different types of workloads like Cron Job, Job and Pod and will be able to see it on Topology page + + + Background: + Given user is at developer perspective + And user is at the Topology page + + + Scenario: Create Cron Job type workload: T-17-TC01 + When user has created or selected namespace "aut-workloads-admin" + And user applies cronjob YAML + And user is at namespace "aut-workloads-admin" + Then user will see cron job with name "example-cronjob" on topology page + + + Scenario: Create Job type workload: T-17-TC02 + When user applies job YAML + And user is at namespace "aut-workloads-admin" + Then user will see job with name "example-job" on topology page + + + Scenario: Create Pod type workload: T-17-TC03 + When user applies pod YAML + And user is at namespace "aut-workloads-admin" + Then user will see pod with name "example-pod" on topology page diff --git a/integration-tests/package.json b/integration-tests/package.json new file mode 100644 index 0000000..da8d7a4 --- /dev/null +++ b/integration-tests/package.json @@ -0,0 +1,14 @@ +{ + "name": "@topology/integration-tests", + "version": "0.0.1", + "description": "Topology Cypress tests", + "private": true, + "cypress-cucumber-preprocessor": { + "step_definitions": "support/step-definitions/*/" + }, + "scripts": { + "test-cypress": "../../../node_modules/.bin/cypress open --env openshift=true", + "test-cypress-headless": "node --max-old-space-size=4096 ../../../node_modules/.bin/cypress run --env openshift=true --browser ${BRIDGE_E2E_BROWSER_NAME:=chrome} --headless cypress:cli,cypress:server:specs --spec \"features/*/topology-ci.feature\"", + "test-cypress-headless-all": "node --max-old-space-size=4096 ../../../node_modules/.bin/cypress run --env openshift=true --browser ${BRIDGE_E2E_BROWSER_NAME:=chrome} --headless cypress:cli,cypress:server:specs" + } +} diff --git a/integration-tests/plugins/index.js b/integration-tests/plugins/index.js new file mode 100644 index 0000000..df54537 --- /dev/null +++ b/integration-tests/plugins/index.js @@ -0,0 +1,87 @@ +const fs = require('fs'); +const webpack = require('@cypress/webpack-preprocessor'); + +module.exports = (on, config) => { + const options = { + webpackOptions: { + resolve: { + extensions: ['.ts', '.tsx', '.js'], + }, + node: { + fs: 'empty', + child_process: 'empty', + readline: 'empty', + }, + module: { + rules: [ + { + test: /\.tsx?$/, + loader: 'ts-loader', + options: { + transpileOnly: true, + }, + }, + { + test: /\.feature$/, + use: [ + { + loader: 'cypress-cucumber-preprocessor/loader', + }, + ], + }, + { + test: /\.features$/, + use: [ + { + loader: 'cypress-cucumber-preprocessor/lib/featuresLoader', + }, + ], + }, + ], + }, + }, + }; + // `on` is used to hook into various events Cypress emits + on('task', { + log(message) { + // eslint-disable-next-line no-console + console.log(message); + return null; + }, + logError(message) { + // eslint-disable-next-line no-console + console.error(message); + return null; + }, + logTable(data) { + // eslint-disable-next-line no-console + console.table(data); + return null; + }, + readFileIfExists(filename) { + if (fs.existsSync(filename)) { + return fs.readFileSync(filename, 'utf8'); + } + return null; + }, + }); + on('file:preprocessor', webpack(options)); + /* In a Docker container, the default size of the /dev/shm shared memory space is 64MB. This is not typically enough + to run Chrome and can cause the browser to crash. You can fix this by passing the --disable-dev-shm-usage flag to + Chrome with the following workaround: */ + on('before:browser:launch', (browser = {}, launchOptions) => { + if (browser.family === 'chromium' && browser.name !== 'electron') { + launchOptions.args.push('--disable-dev-shm-usage'); + } + return launchOptions; + }); + // `config` is the resolved Cypress config + config.baseUrl = `${process.env.BRIDGE_BASE_ADDRESS || 'http://localhost:9000'}${( + process.env.BRIDGE_BASE_PATH || '/' + ).replace(/\/$/, '')}`; + config.env.BRIDGE_HTPASSWD_IDP = process.env.BRIDGE_HTPASSWD_IDP; + config.env.BRIDGE_HTPASSWD_USERNAME = process.env.BRIDGE_HTPASSWD_USERNAME; + config.env.BRIDGE_HTPASSWD_PASSWORD = process.env.BRIDGE_HTPASSWD_PASSWORD; + config.env.BRIDGE_KUBEADMIN_PASSWORD = process.env.BRIDGE_KUBEADMIN_PASSWORD; + return config; +}; diff --git a/integration-tests/reporter-config.json b/integration-tests/reporter-config.json new file mode 100644 index 0000000..2244941 --- /dev/null +++ b/integration-tests/reporter-config.json @@ -0,0 +1,14 @@ +{ + "reporterEnabled": "mocha-junit-reporter, mochawesome", + "mochaJunitReporterReporterOptions": { + "mochaFile": "../../../gui_test_screenshots/junit_cypress-[hash].xml", + "toConsole": false + }, + "mochawesomeReporterOptions": { + "reportDir": "../../../gui_test_screenshots/", + "reportFilename": "cypress_report_topology", + "overwrite": false, + "html": false, + "json": true + } +} diff --git a/integration-tests/support/commands/hooks.ts b/integration-tests/support/commands/hooks.ts new file mode 100644 index 0000000..5803ae2 --- /dev/null +++ b/integration-tests/support/commands/hooks.ts @@ -0,0 +1,15 @@ +/* eslint-disable no-console, promise/catch-or-return */ +before(() => { + cy.login(); + cy.document() + .its('readyState') + .should('eq', 'complete'); +}); + +after(() => { + const namespaces: string[] = Cypress.env('NAMESPACES') || []; + cy.exec(`oc delete namespace ${namespaces.join(' ')}`, { + failOnNonZeroExit: false, + timeout: 180000, + }); +}); diff --git a/integration-tests/support/commands/index.ts b/integration-tests/support/commands/index.ts new file mode 100644 index 0000000..b9d5f4e --- /dev/null +++ b/integration-tests/support/commands/index.ts @@ -0,0 +1,8 @@ +// Include the cypress customized commands related files +import '../../../../integration-tests-cypress/support/selectors'; +import '../../../../integration-tests-cypress/support/a11y'; +import '../../../../integration-tests-cypress/support/login'; +import '../../../../integration-tests-cypress/support/project'; +import '../../../../integration-tests-cypress/support/index'; +import '../../../../dev-console/integration-tests/support/commands/app'; +import './hooks'; diff --git a/integration-tests/support/page-objects/chart-area-po.ts b/integration-tests/support/page-objects/chart-area-po.ts new file mode 100644 index 0000000..3c2a534 --- /dev/null +++ b/integration-tests/support/page-objects/chart-area-po.ts @@ -0,0 +1,48 @@ +export const chartAreaPO = { + editApplicationGrouping: '[data-test-action="Edit application grouping"]', + editPodCount: '[data-test-action="Edit Pod count"]', + pauseRollouts: '[data-test-action="Pause rollouts"]', + addHealthChecks: '[data-test-action="Add Health Checks"]', + addHorizontalPod: '[data-test-action="Add HorizontalPodAutoscaler"]', + addStorage: '[data-test-action="Add storage"]', + editUpdateStrategy: '[data-test-action="Edit update strategy"]', + editLabels: '[data-test-action="Edit labels"]', + editAnnotations: '[data-test-action="Edit annotations"]', + editDeployment: '[data-test-action="Edit Deployment"]', + deleteDeployment: '[data-test-action="Delete Deployment"]', + topologyArea: '[data-test-id="topology"]', + addToProject: '.odc-topology-context-menu', + samples: '[data-test-action="Samples"]', + importFromGit: '[data-test-action="Import from Git"]', + containerImage: '[data-test-action="Container Image"]', + catalog: '[data-test-action="From Catalog"]', + database: '[data-test-action="Database"]', + operatorBacked: '[data-test-action="Operator Backed"]', + helmCharts: '[data-test-action="Helm Charts"]', + eventsource: '[data-test-action="Event Source"]', + channel: '[data-test-action="Channel"]', + filterItem: 'input[placeholder="Filter by keyword..."]', + sampleGo: '[data-test="Sample-Go"]', + submitButton: '[data-test-id="submit-button"]', + deployImage: '[data-test-id="deploy-image-search-term"]', + validationText: '#form-input-searchTerm-field-helper', + pythonBuilderImage: '[data-test="BuilderImage-Python"]', + overlayCreate: '.co-catalog-page__overlay-action', + gitInputURL: '[data-test-id="git-form-input-url"]', + postgresqlTemplate: '[data-test="Template-PostgreSQL"]', + operatorBackedPostgres: '[data-test="OperatorBackedService-Postgres Cluster"]', + operatorBackedRedis: '[data-test="OperatorBackedService-Redis"]', + helmNodejs: '[data-test="HelmChart-Nodejs"]', + helmReleaseName: '#form-input-releaseName-field', + apiEventSource: '[data-test="EventSource-ApiServerSource"]', + eventName: '[data-test="pairs-list-name"]', + eventValue: '[data-test="pairs-list-value"]', + channelName: '[data-test-id="channel-name"]', + saveChanges: '[data-test="save-changes"]', + yamlEditor: 'div.monaco-scrollable-element.editor-scrollable.vs-dark', + yamlView: '[data-test="import-yaml"]', + dangerAlert: '[aria-label="Danger Alert"]', + infoAlert: '[aria-label="Info Alert"]', + contentScrollable: '#content-scrollable', + gitForm: '[data-test-id="import-git-form"]', +}; diff --git a/integration-tests/support/page-objects/export-applications-po.ts b/integration-tests/support/page-objects/export-applications-po.ts new file mode 100644 index 0000000..9654882 --- /dev/null +++ b/integration-tests/support/page-objects/export-applications-po.ts @@ -0,0 +1,38 @@ +export const exportApplication = { + exportApplicationButton: '[data-test="export-app-btn"]', + infoTip: '[aria-label="Info Alert"]', + exportView: '[data-test="export-view-log-btn"]', + resourceAddedNotification: '[aria-label="Close Info alert: alert: Resource added"]', +}; + +export const buttonDisplayName = (buttonName: string) => { + switch (buttonName) { + case 'View logs': { + return 'export-view-log-btn'; + } + case 'Cancel Export': { + return 'export-cancel-btn'; + } + case 'Restart Export': { + return 'export-restart-btn'; + } + case 'Ok': { + return 'export-close-btn'; + } + default: { + throw new Error('Option is not available'); + } + } +}; + +export function exportModalButton(element: string) { + const buttonName = buttonDisplayName(element); + return `[data-test~="${buttonName}"]`; +} + +export function closeExportNotification() { + return cy + .get('[aria-label="Close Info alert: alert: Export application"]') + .should('be.visible') + .click(); +} diff --git a/integration-tests/support/page-objects/hpa-po.ts b/integration-tests/support/page-objects/hpa-po.ts new file mode 100644 index 0000000..34a519a --- /dev/null +++ b/integration-tests/support/page-objects/hpa-po.ts @@ -0,0 +1,27 @@ +export const hpaPO = { + navWorkloads: '[data-quickstart-id="qs-nav-workloads"]', + nav: '[data-test="nav"]', + createItem: '[data-test="item-create"]', + saveChanges: '[data-test="save-changes"]', + resourceTitle: '[data-test-id="resource-title"]', + hpaHeading: '[data-test-section-heading="HorizontalPodAutoscaler details"]', + itemFilter: '[data-test-id="item-filter"]', + resourceItem: '[class="co-resource-item"]', + actionMenu: '[data-test-id="actions-menu-button"]', + modalTitle: '[data-test-id="modal-title"]', + deleteOption: '[data-test="confirm-action"]', + exampleHPA: '[data-test-id="example"]', + emptyMessage: '[data-test="empty-message"]', + nameHPA: '[id="form-input-formData-metadata-name-field"]', + minhpaPod: '[id="form-number-spinner-formData-spec-minReplicas-field"]', + maxhpaPod: '[id="form-number-spinner-formData-spec-maxReplicas-field"]', + cpu: '[id="cpu"]', + memory: '[id="memory"]', + submitBtn: '[data-test-id="submit-button"]', + sectionHeading: '.sidebar__section-heading', + sidebarClose: '[data-test-id="sidebar-close-button"]', + switchToggler: '[data-test-id="topology-switcher-view"]', + hpaFormName: '[id="form-input-formData-metadata-name-field"]', + podRing: '[class="odc-pod-ring"]', + nodeList: '[class="list-group-item container-fluid"]', +}; diff --git a/integration-tests/support/page-objects/topology-add-options-po.ts b/integration-tests/support/page-objects/topology-add-options-po.ts new file mode 100644 index 0000000..2fc3a19 --- /dev/null +++ b/integration-tests/support/page-objects/topology-add-options-po.ts @@ -0,0 +1,3 @@ +export const topologyAddOptionsPO = (optionName: string) => { + return `[data-test-action="${optionName}"]`; +}; diff --git a/integration-tests/support/page-objects/topology-po.ts b/integration-tests/support/page-objects/topology-po.ts new file mode 100644 index 0000000..b2d8d9c --- /dev/null +++ b/integration-tests/support/page-objects/topology-po.ts @@ -0,0 +1,336 @@ +export const topologyPO = { + switcher: 'button[data-test-id="topology-switcher-view"]', + noWorkLoadsText: 'h2.co-hint-block__title', + title: 'h1.ocs-page-layout__title', + search: '[data-test-id="item-filter"]', + resetView: '[id="reset-view"]', + clearFilter: '[class="pf-c-toolbar__item"]', + emptyStateIcon: 'div.pf-c-empty-state__icon', + emptyText: '[data-test="no-resources-found"]', + addToApplication: '[data-test-action="add-to-application"]', + addToApplicationInContext: 'button.pf-topology-context-sub-menu.pf-c-dropdown__menu-item', + quickSearch: '[data-test="quick-search-bar"]', + filterByResourceDropDown: '[data-test="filter-by-resource"] button', + topologyDropDown: 'button[aria-label="Options menu"]', + emptyView: { + startBuildingYourApplicationLink: '[data-test="start-building-your-application"]', + addPageLink: '[data-test="add-page"]', + }, + graph: { + reset: '#reset-view', + layoutViewGroup: '.odc-topology__layout-group', + zoomIn: '#zoom-in', + zoomOut: '#zoom-out', + saveModal: '[data-test="confirm-action"]', + modalContent: '[class="modal-content"]', + fitToScreen: '#fit-to-screen', + emptyGraph: '[data-test-id="topology"]', + filterDropdown: '[id^=pf-select-toggle-id]', + nodeContextMenu: '.pf-c-dropdown__menu-item', + nodeLabel: 'g.pf-topology__node__label', + knativeNodeLabel: '.odc-base-node__label', + groupLabel: 'g.pf-topology__group__label', + selectNodeLabel: 'g.odc-base-node__label', + knativeServiceNode: '[data-type="knative-service"]', + eventSourceNode: '[data-type="event-source-link"]', + contextMenu: '#popper-container ul', + workloads: 'g[data-surface="true"]', + node: '[data-test-id="base-node-handler"]', + workload: '[data-type="workload"]', + triggerLink: '[data-type="event-pubsub-link"]', + triggerEdgeLink: '[class="pf-topology__edge__link"]', + confirmModal: '[data-test="confirm-action"]', + deleteWorkload: '[data-test="confirm-action"]', + eventSourceWorkload: '[data-type="event-source"]', + applicationGroupingTitle: '.odc-topology-list-view__application-label', + addNewAnnotations: '[data-test="add-button"]', + deleteApplication: '[id="form-input-resourceName-field"]', + connector: '[data-test-id="edge-handler"]', + routeDecorator: '[aria-label="Open URL"]', + subscriber: { + dropdown: '[id="form-ns-dropdown-spec-subscriber-ref-name-field"]', + filter: '[class="pf-c-dropdown__toggle-text"]', + filterItemLink: '[data-test="dropdown-menu-item-link"]', + filterText: '[data-test-id="dropdown-text-filter"]', + filterField: '[id="form-ns-dropdown-ref-name-field"]', + }, + displayOptions: { + connectivityMode: '[id="showGroups"]', + consumptionMode: '[id="hideGroups"]', + expandSwitchToggle: '.pf-c-switch__input', + applicationGroupings: '[id$=expand-app-groups]', + showLabels: '[id$=show-labels]', + showPodCount: '[id$=show-pod-count]', + }, + contextMenuOptions: { + addToProject: '.pf-topology-context-sub-menu', + }, + addLink: '[data-test="add-page"]', + quickSearch: '[data-test="quick-search-bar"]', + warningBackground: '[class="pf-topology__node__background pf-m-warning"]', + }, + list: { + appName: '#HelmRelease ul li div', + nodeName: '#HelmRelease ul li div', + resourceTitle: 'pf-c-data-list__cell.odc-topology-list-view__kind-label', + switcher: '[data-test-id="topology-switcher-view"][aria-label="Graph view"]', + view: '[aria-label="Topology List View"]', + switchGraph: '[aria-label="Graph view"]', + }, + sidePane: { + actionsDropDown: '[data-test-id="actions-menu-button"]', + showPodCount: '[id$=show-pod-count]', + dialog: '[role="dialog"]', + title: '[role="dialog"] h1', + knativeServiceIcon: '[title="Service"]', + tabs: '[role="dialog"] li button', + sectionTitle: 'h2', + close: 'button[aria-label="Close"]', + labelsList: '[data-test="label-list"]', + editAnnotations: '[data-test="edit-annotations"]', + tabName: '[role="dialog"] li button', + healthCheckAlert: 'div.odc-topology-sidebar-alert', + resourceQuotaAlert: 'div.odc-topology-sidebar-alert [aria-label="Warning Alert"]', + podScale: 'button.pf-c-button.pf-m-plain.pf-m-block', + podText: 'text.pf-chart-donut-title.pod-ring__center-text', + applicationGroupingsTitle: '.overview__sidebar-pane-head.resource-overview__heading', + applicationGroupingsSidepane: 'overview__sidebar-pane resource-overview', + resourcesTabApplicationGroupings: '.co-m-horizontal-nav__menu-item', + pipelineRunsDetails: '.sidebar__section-heading', + pipelineRunsLogSnippet: '.ocs-log-snippet__log-snippet', + pipelineRunsStatus: '.ocs-log-snippet__status-message', + pipelineRunsLinks: 'a.sidebar__section-view-all', + detailsTab: { + labels: '[data-test="label-list"]', + annotations: '[data-test="edit-annotations"]', + labelsEdit: '[data-test="Labels-details-item__edit-button"]', + }, + resourcesTab: { + startLastRun: '[role="dialog"] li.list-group-item.pipeline-overview div button', + pipelineRuns: 'li.odc-pipeline-run-item', + routeLink: '[data-test-id="route-link"]', + waitingPods: 'button[data-test="waiting-pods"]', + podTrafficStatus: 'div[data-test="pod-traffic-status', + }, + monitoringTab: { + viewMonitoringDashBoardLink: '[data-test="observe-dashboard-link"]', + }, + releaseNotesTab: {}, + }, + addStorage: { + pvc: { + useExistingClaim: 'input[value="existing"]', + createNewClaim: { + newClaim: 'input[value="new]', + storageClass: '#storageclass-dropdown', + pvcName: '#pvc-name', + accessMode: { + singleUser: 'input[value="ReadWriteOnce"]', + sharedAccess: 'inputp[value="ReadWriteMany"]', + readOnly: 'input[value="ReadOnlyMany"]', + size: '#request-size-input', + showLabelSelector: 'input[name="showLabelSelector"]', + }, + volumeMode: { + fileSystem: 'input[value="Filesystem"]', + block: 'input[value="Block"]', + devicePath: '#device-path', + }, + }, + }, + mountPath: '#mount-path', + subPath: '#subpath', + mountAsReadOnly: 'input[name="mountAsReadOnly"]', + save: '#save-changes', + }, + revisionDetails: { + detailsTab: '[data-test-id="horizontal-link-Details"]', + yamlTab: '[data-test-id="horizontal-link-YAML"]', + details: { + resourceSummaryTitle: '[data-test-section-heading="Revision Details"]', + resourceSummary: '[data-test-id="resource-summary"]', + conditionsTitle: '[data-test-section-heading="Conditions"]', + }, + yaml: { + save: '[data-test="save-changes"]', + reload: '[data-test="reload-object"]', + cancel: '[data-test="cancel"]', + }, + }, + highlightNode: '.is-filtered', + createSecret: { + advancedOptions: '.pf-c-expandable-section__toggle-text', + secretForm: '.co-create-secret-form.modal-content', + createSecretButton: 'button.pf-c-button.pf-m-link.pf-m-link--align-left', + secretDropDown: '[id="form-ns-dropdown-formData-imagePullSecret-field"]', + secretDropDownItem: '[data-test="dropdown-menu-item-link"]', + formInputs: { + secretFormTitle: '[data-test-id="modal-title"]', + secretName: '[id="secret-name"]', + authenticationType: '[data-test-id="dropdown-button"]', + imageRegistryCredentials: '[data-test-dropdown-menu="credentials"]', + uploadConfigurationFile: '[data-test-dropdown-menu="config-file"]', + registryServerAddress: 'input[name="address"]', + userName: 'input[name="username"]', + password: 'input[name="password"]', + email: 'input[name="email"]', + saveSecret: '[data-test="confirm-action"]', + reloadForm: '[data-test-id="reset-button"]', + cancelAction: '[data-test-id="cancel-button"]', + }, + }, + resourceLimits: { + requestCPU: '[aria-describedby="form-resource-limit-limits-cpu-request-field-helper"]', + limitCPU: '[aria-describedby="form-resource-limit-limits-cpu-limit-field-helper"]', + requestMemory: '[aria-describedby="form-resource-limit-limits-memory-request-field-helper"]', + limitMemory: '[aria-describedby="form-resource-limit-limits-memory-limit-field-helper"]', + }, + deploymentStrategy: { + strategyTypeDropDown: 'button[id="form-dropdown-formData-deploymentStrategy-type-field"]', + recreateStrategy: 'button[id="Recreate-link"]', + rollingUpdate: 'button[id="RollingUpdate-link"]', + customUpdate: 'button[id="Custom-link"]', + maxUnavailablePods: 'input[name="formData.deploymentStrategy.rollingUpdate.maxUnavailable"]', + maxSurgePods: 'input[name="formData.deploymentStrategy.rollingUpdate.maxSurge"]', + projectDropDown: '[id="form-ns-dropdown-formData-imageStream-namespace-field"]', + imageStream: '[id="form-ns-dropdown-formData-imageStream-image-field"]', + tag: '[id="form-dropdown-formData-imageStream-tag-field"]', + envRow: '[data-test="pairs-list-row"]', + envName: '[data-test="pairs-list-name"]', + envValue: '[data-test="pairs-list-value"]', + advancedOptions: 'button.pf-c-button.pf-m-link.pf-m-inline', + pauseRolloutsCheckbox: '[id="form-checkbox-formData-paused-field"]', + enterReplica: 'input[id="form-number-spinner-formData-replicas-field"]', + saveEdit: '[data-test-id="submit-button"]', + selectSecret: '[id="form-ns-dropdown-formData-imagePullSecret-field"]', + dropdownSecret: '[data-test-id="dropdown-text-filter"]', + timeout: + 'input[id="form-input-formData-deploymentStrategy-recreateParams-timeoutSeconds-field"]', + deployImageCheckbox: 'input[name="formData.fromImageStreamTag"]', + imageName: 'input[name="formData.imageName"]', + preLifecycleHook: { + preExecNewPod: + 'input[id="form-radiobutton-formData-deploymentStrategy-recreateParams-pre-action-execNewPod-field"]', + preExecNewPodContainerDD: + '[id="form-dropdown-formData-deploymentStrategy-recreateParams-pre-lch-execNewPod-containerName-field"]', + runCommand: + '[id="form-input-formData-deploymentStrategy-recreateParams-pre-lch-execNewPod-command-0-field"]', + preTagImagesField: + 'input[id="form-radiobutton-formData-deploymentStrategy-recreateParams-pre-action-tagImages-field"]', + preTagImagesFieldContainerDD: + '[id="form-dropdown-formData-deploymentStrategy-imageStreamData-pre-containerName-field"]', + projectDropDown: + 'button[id="form-ns-dropdown-formData-deploymentStrategy-imageStreamData-pre-imageStream-namespace-field"]', + imageStream: + 'button[id="form-ns-dropdown-formData-deploymentStrategy-imageStreamData-pre-imageStream-image-field"]', + imageStreamTag: + 'button[id="form-dropdown-formData-deploymentStrategy-imageStreamData-pre-imageStream-tag-field"]', + failurePolicy: + 'button[id="form-dropdown-formData-deploymentStrategy-recreateParams-pre-lch-failurePolicy-field"]', + }, + postLifecycleHook: { + postExecNewPod: + 'input[id="form-radiobutton-formData-deploymentStrategy-recreateParams-post-action-execNewPod-field"]', + postExecNewPodContainerNameDD: + '[id="form-dropdown-formData-deploymentStrategy-recreateParams-post-lch-execNewPod-containerName-field"]', + runCommand: + 'input[id="form-input-formData-deploymentStrategy-recreateParams-post-lch-execNewPod-command-0-field"]', + postTagImagesField: + 'input[id="form-radiobutton-formData-deploymentStrategy-recreateParams-post-action-tagImages-field"]', + postTagImagesFieldContainerDD: + 'button[id="form-dropdown-formData-deploymentStrategy-imageStreamData-post-containerName-field"]', + projectDropDown: + 'button[id="form-ns-dropdown-formData-deploymentStrategy-imageStreamData-post-imageStream-namespace-field"]', + imageStream: + 'button[id="form-ns-dropdown-formData-deploymentStrategy-imageStreamData-post-imageStream-image-field"]', + imageStreamTag: + 'button[id="form-dropdown-formData-deploymentStrategy-imageStreamData-post-imageStream-tag-field"]', + failurePolicy: + 'button[id="form-dropdown-formData-deploymentStrategy-recreateParams-post-lch-failurePolicy-field"]', + }, + midLifecycleHook: { + midExecNewPod: + 'input[id="form-radiobutton-formData-deploymentStrategy-recreateParams-mid-action-execNewPod-field"]', + midContainerNameDropDown: + 'button[id="form-dropdown-formData-deploymentStrategy-recreateParams-mid-lch-execNewPod-containerName-field"]', + runCommand: + 'id="form-input-formData-deploymentStrategy-recreateParams-mid-lch-execNewPod-command-0-field"', + midTagImagesField: + 'input[id="form-radiobutton-formData-deploymentStrategy-recreateParams-mid-action-tagImages-field"]', + midTagImagesFieldContainerDD: + 'button[id="form-dropdown-formData-deploymentStrategy-imageStreamData-mid-containerName-field"]', + projectDropDown: + 'button[id="form-ns-dropdown-formData-deploymentStrategy-imageStreamData-mid-imageStream-namespace-field"]', + imageStream: + 'button[id="form-ns-dropdown-formData-deploymentStrategy-imageStreamData-mid-imageStream-image-field"]', + imageStreamTag: + 'button[id="form-dropdown-formData-deploymentStrategy-imageStreamData-mid-imageStream-tag-field"]', + failurePolicy: + 'button[id="form-dropdown-formData-deploymentStrategy-recreateParams-mid-lch-failurePolicy-field"]', + }, + tickButton: '[data-test-id="check-icon"]', + }, + grouping: { + addToApplication: '[data-test-action="add-to-application"]', + importFromGitOption: '[data-test-action="Import from Git"]', + filterResources: '[data-test="filter-by-resource"]', + deploymentCheckbox: '[data-test="Deployment"]', + }, + quickSearchPO: { + listView: '[aria-label="List view"]', + graphView: '[aria-label="Graph view"]', + toggleView: '[data-test-id="topology-switcher-view"]', + noResults: '[data-test="quick-search-no-results"]', + quickstartDrawer: '[data-test="quickstart drawer"]', + quickStarts: '#quickStarts', + pageTitle: '[data-test="page-title"]', + submitBtn: '[data-test-id="submit-button"]', + samplePage: '#Samples', + resourseTitle: '[data-test-id="resource-title"]', + appformName: '[data-test-id="application-form-app-name"]', + djangoPostgreSQL: '[data-test="item-name-Django + PostgreSQL-Templates"]', + NETSample: '[data-test="item-name-.NET-Samples"]', + monitorApp: '[data-test="item-name-Monitor your sample application-Quick Starts"]', + nodejsDevfiles: '[data-test="item-name-Basic Node.js-Devfiles"]', + nodejsSamples: '[data-test="item-name-Basic Node.js-Samples"]', + }, + toolbarFilterPO: { + deployment: '[data-test="Deployment"]', + deploymentConfig: '[data-test="DeploymentConfig"]', + deploymentSpan: '[data-test="Deployment"] span', + deploymentConfigSpan: '[data-test="DeploymentConfig"] span', + deploymentCheckbox: '[data-test="Deployment"] input', + deploymentConfigCheckbox: '[data-test="DeploymentConfig"] input', + deploymentApp: '#nodejs-ex-git-app-Deployment', + deploymentConfigApp: '#nodejs-ex-git-app-DeploymentConfig', + }, + displayFilter: { + display: '.odc-topology-filter-dropdown__select', + expandOption: '.odc-topology-filter-dropdown__expand-groups-switcher input', + applicationGroupingOption: '.odc-topology-filter-dropdown__expand-groups-label input', + unexpandedNode: '.odc-workload-node', + disabledClass: '.pf-m-disabled', + podLabelOptions: '.odc-topology-filter-dropdown__group input', + podRingText: '.pod-ring__center-text', + deploymentLabel: '#nodejs-ex-git-app-Deployment-label', + deployemntCount: '.odc-topology-list-view__group-resource-count', + }, + pipelines: { + storageNav: '[data-quickstart-id="qs-nav-storage"]', + pvcOption: '[href="/k8s/all-namespaces/persistentvolumeclaims"]', + pvc: '[aria-label="PersistentVolumeClaims"]', + startAction: '[data-test-action="Start"]', + pvcIcon: '.co-m-resource-persistentvolumeclaim', + addTriggerAction: '[data-test-action="Add Trigger"]', + pipelineCheckbox: '#form-checkbox-pipeline-enabled-field', + editWorkloadPage: '#content-scrollable', + pipelineSection: '.odc-form-section-pipeline', + }, +}; + +export const typeOfWorkload = (workload: string) => { + return `[data-id="odc-topology-graph"] .odc-resource-icon-${workload + .toLowerCase() + .replace(' ', '') + .trim()}`; +}; diff --git a/integration-tests/support/pages/export-application/export-applications.ts b/integration-tests/support/pages/export-application/export-applications.ts new file mode 100644 index 0000000..d42098a --- /dev/null +++ b/integration-tests/support/pages/export-application/export-applications.ts @@ -0,0 +1,42 @@ +import { exportApplication, exportModalButton } from '../../page-objects/export-applications-po'; + +export function clickVisibleButton(flag: boolean = true) { + /* eslint-disable promise/catch-or-return */ + + cy.get('body').then(($body) => { + if ( + $body + .find('.modal-body') + .text() + .includes(' is in progress') + ) { + cy.log('Close progress modal'); + cy.get(exportModalButton('Ok')) + .should('be.visible') + .click(); + cy.get(exportModalButton('Ok')).should('not.exist'); + cy.get(exportApplication.exportApplicationButton) + .should('be.visible') + .click(); + if (flag === true) { + cy.get(exportModalButton('Restart Export')) + .should('be.visible') + .click(); + cy.get(exportModalButton('Restart Export')).should('not.exist'); + } + } + }); +} + +export const exportOfApplication = { + exportApplicationFresh: () => { + /* eslint-disable promise/catch-or-return */ + + cy.get(exportApplication.exportApplicationButton) + .should('be.visible') + .click(); + cy.byTestID('close-btn') + .should('be.visible') + .click(); + }, +}; diff --git a/integration-tests/support/pages/functions/add-secret.ts b/integration-tests/support/pages/functions/add-secret.ts new file mode 100644 index 0000000..9baec65 --- /dev/null +++ b/integration-tests/support/pages/functions/add-secret.ts @@ -0,0 +1,60 @@ +import { app, topologySidePane } from '@console/dev-console/integration-tests/support/pages'; +import { editDeployment } from '@console/topology/integration-tests/support/pages/topology/topology-edit-deployment'; +import { hpaPO } from '../../page-objects/hpa-po'; +import { topologyPO } from '../../page-objects/topology-po'; + +export const addSecret = ( + secretName: string = 'newSecret 1', + serverUrl: string = 'https://quay.io/repository/kubernetes-ingress-controller/nginx-ingress-controller?tag=latest&tab=tags', + username: string = 'test1', + password: string = 'test', + email: string = 'test1@redhat.com', +) => { + editDeployment.verifyModalTitle(); + editDeployment.addSecretName(secretName); + editDeployment.addServerAddress(serverUrl); + editDeployment.enterUsername(username); + editDeployment.enterPassword(password); + editDeployment.enterEmail(email); + editDeployment.saveSecret(); +}; + +export const checkPodsText = (tries: number = 5) => { + if (tries < 1) { + return; + } + // eslint-disable-next-line promise/catch-or-return + cy.get('body').then(($body) => { + if ( + !$body + .find(topologyPO.sidePane.podText) + .text() + .includes('1Pod') + ) { + cy.reload(); + app.waitForDocumentLoad(); + topologySidePane.selectTab('Details'); + cy.wait(35000); + checkPodsText(tries - 1); + } else { + cy.get(topologyPO.sidePane.podText, { timeout: 120000 }).should('have.text', '1Pod'); + } + }); +}; + +export const checkPodsCount = (tries: number = 5) => { + if (tries < 1) { + return; + } + // eslint-disable-next-line promise/catch-or-return + cy.get('body').then(($body) => { + if ($body.find(hpaPO.nodeList).length === 0) { + cy.reload(); + app.waitForDocumentLoad(); + cy.wait(35000); + checkPodsCount(tries - 1); + } else { + cy.log('Found'); + } + }); +}; diff --git a/integration-tests/support/pages/functions/chart-functions.ts b/integration-tests/support/pages/functions/chart-functions.ts new file mode 100644 index 0000000..c987ba2 --- /dev/null +++ b/integration-tests/support/pages/functions/chart-functions.ts @@ -0,0 +1,127 @@ +import { devNavigationMenu } from '@console/dev-console/integration-tests/support/constants'; +import { + app, + gitPage, + navigateTo, + yamlEditor, +} from '@console/dev-console/integration-tests/support/pages'; +import { chartAreaPO } from '../../page-objects/chart-area-po'; +import { topologyPO } from '../../page-objects/topology-po'; +import { topologyHelper, topologyPage } from '../topology'; + +export const verifyMultipleWorkloadInTopologyPage = (workloadNames: string[]) => { + // eslint-disable-next-line @typescript-eslint/prefer-for-of + for (let i = 0; i < workloadNames.length; i++) { + const name = workloadNames[i]; + topologyHelper.search(name); + topologyPage.verifyUserIsInGraphView(); + cy.get(topologyPO.graph.fitToScreen).click(); + cy.get(topologyPO.highlightNode).should('be.visible'); + app.waitForDocumentLoad(); + } +}; + +export const createWorkloadUsingOptions = (optionName: string, optionalData?: string) => { + switch (optionName) { + case 'Go Sample': + cy.get(chartAreaPO.submitButton) + .should('be.enabled') + .click(); + break; + + case 'Import From Git': + gitPage.enterGitUrl('https://github.com/sclorg/nodejs-ex.git'); + gitPage.verifyValidatedMessage('https://github.com/sclorg/nodejs-ex.git'); + gitPage.enterComponentName('nodejs-ex-git'); + gitPage.selectResource('Deployment'); + gitPage.enterAppName('nodejs-ex-git-app'); + cy.get(chartAreaPO.submitButton).click(); + break; + + case 'Container Image': + cy.get(chartAreaPO.deployImage) + .clear() + .type('openshift/hello-openshift'); + cy.get(chartAreaPO.validationText).should('have.text', 'Validated'); + cy.get(chartAreaPO.submitButton).click(); + break; + + case 'Catalog': + cy.get(chartAreaPO.gitInputURL).type('https://github.com/sclorg/nodejs-ex.git'); + gitPage.verifyValidatedMessage('https://github.com/sclorg/nodejs-ex.git'); + gitPage.enterComponentName('python-app'); + gitPage.selectResource('Deployment'); + gitPage.enterAppName('nodejs-ex-git-app'); + cy.get(chartAreaPO.submitButton).click(); + break; + + case 'Operator Backed': + cy.get(chartAreaPO.yamlView).click(); + yamlEditor.isLoaded(); + cy.get(chartAreaPO.yamlEditor) + .click() + .focused() + .type('{ctrl}a') + .clear(); + + // eslint-disable-next-line no-case-declarations + const yamlLocation = `support/${optionalData}`; + yamlEditor.setEditorContent(yamlLocation); + cy.get(chartAreaPO.saveChanges).click(); + cy.get('[aria-label="Breadcrumb"]').should('contain', 'Redis details'); + navigateTo(devNavigationMenu.Topology); + break; + + case 'Helm Chart': + cy.get(chartAreaPO.helmReleaseName) + .clear() + .type('helm-nodejs'); + cy.get(chartAreaPO.submitButton).click(); + break; + + case 'Event Source': + cy.get(chartAreaPO.eventName).type('v11'); + cy.get(chartAreaPO.eventValue).type('kind'); + cy.get(chartAreaPO.submitButton).click(); + break; + + case 'Channel': + cy.get(chartAreaPO.channelName).should('have.value', 'channel'); + cy.get(chartAreaPO.submitButton).click(); + break; + + default: + break; + } +}; + +export const addToProjectOptions = (optionName: string) => { + switch (optionName) { + case 'Import from Git': + cy.get(chartAreaPO.importFromGit).click(); + break; + case 'Container Image': + cy.get(chartAreaPO.containerImage).click(); + break; + case 'From Catalog': + cy.get(chartAreaPO.catalog).click(); + break; + case 'Database': + cy.get(chartAreaPO.database).click(); + break; + case 'Operator Backed': + cy.get(chartAreaPO.operatorBacked).click(); + break; + case 'Helm Charts': + cy.get(chartAreaPO.helmCharts).click(); + break; + case 'Event Source': + cy.get(chartAreaPO.eventsource).click(); + break; + case 'Channel': + cy.get(chartAreaPO.channel).click(); + break; + default: + break; + } +}; diff --git a/integration-tests/support/pages/topology/index.ts b/integration-tests/support/pages/topology/index.ts new file mode 100644 index 0000000..f382eaf --- /dev/null +++ b/integration-tests/support/pages/topology/index.ts @@ -0,0 +1,4 @@ +export * from './topology-page'; +export * from './topology-actions-page'; +export * from './topology-helper-page'; +export * from './topology-side-pane-page'; diff --git a/integration-tests/support/pages/topology/topology-actions-page.ts b/integration-tests/support/pages/topology/topology-actions-page.ts new file mode 100644 index 0000000..c07d65c --- /dev/null +++ b/integration-tests/support/pages/topology/topology-actions-page.ts @@ -0,0 +1,230 @@ +import { detailsPage } from '@console/cypress-integration-tests/views/details-page'; +import { modal } from '@console/cypress-integration-tests/views/modal'; +import { + nodeActions, + addToApplicationGroupings, + applicationGroupingsActions, +} from '@console/dev-console/integration-tests/support/constants'; +import { topologyPO } from '../../page-objects/topology-po'; + +export const topologyActions = { + selectAction: (action: nodeActions | string | applicationGroupingsActions) => { + switch (action) { + case 'Edit Application Grouping': + case 'Edit application grouping': + case nodeActions.EditApplicationGrouping: { + cy.byTestActionID(action) + .should('be.visible') + .click(); + break; + } + case 'Edit Deployment': + case nodeActions.EditDeployment: { + cy.byTestActionID(action) + .should('be.visible') + .click(); + break; + } + case 'Edit DeploymentConfig': + case nodeActions.EditDeploymentConfig: { + cy.byTestActionID(action) + .should('be.visible') + .click(); + break; + } + case 'Edit Pod Count': + case 'Edit Pod count': + case nodeActions.EditPodCount: { + cy.byTestActionID(action) + .should('be.visible') + .click(); + break; + } + case 'Edit Labels': + case 'Edit labels': + case nodeActions.EditLabels: { + cy.byTestActionID(action) + .should('be.visible') + .click(); + cy.get('form').should('be.visible'); + modal.modalTitleShouldContain('Edit labels'); + break; + } + case 'Edit Annotations': + case 'Edit annotations': + case nodeActions.EditAnnotations: { + cy.byTestActionID(action) + .should('be.visible') + .click(); + cy.get('form').should('be.visible'); + modal.modalTitleShouldContain('Edit annotations'); + break; + } + case 'Edit Update Strategy': + case nodeActions.EditUpdateStrategy: { + cy.byTestActionID(action) + .should('be.visible') + .click(); + break; + } + case 'Delete Deployment': + case nodeActions.DeleteDeployment: { + cy.byTestActionID(action) + .should('be.visible') + .click(); + break; + } + case 'Delete DeploymentConfig': + case nodeActions.DeleteDeploymentConfig: { + cy.byTestActionID(action) + .should('be.visible') + .click(); + break; + } + case 'Delete SinkBinding': + case nodeActions.DeleteSinkBinding: { + cy.byTestActionID(action) + .should('be.visible') + .click(); + break; + } + case 'Edit SinkBinding': + case nodeActions.EditSinkBinding: { + cy.byTestActionID(action) + .should('be.visible') + .click(); + break; + } + case 'Edit resource limits': + case nodeActions.EditResourceLimits: { + cy.byTestActionID(action) + .should('be.visible') + .click(); + break; + } + case 'Move sink': + case nodeActions.MoveSink: { + cy.byTestActionID(action) + .should('be.visible') + .click(); + break; + } + case 'Delete Service': + case nodeActions.DeleteService: { + cy.byTestActionID(action) + .should('be.visible') + .click(); + break; + } + case 'Make Serverless': + case nodeActions.MakeServerless: { + cy.byTestActionID(action).click(); + detailsPage.titleShouldContain(action); + break; + } + case 'Create Service Binding': + case nodeActions.CreateServiceBinding: { + cy.byTestActionID(action) + .scrollIntoView() + .should('be.visible') + .click(); + break; + } + case 'Delete application': + case applicationGroupingsActions.DeleteApplication: { + cy.byTestActionID(action) + .should('be.visible') + .click(); + break; + } + case 'Add to application': + case applicationGroupingsActions.AddtoApplication: { + cy.get(topologyPO.addToApplication) + .should('be.visible') + .click(); + break; + } + case 'Add Health Checks': + case nodeActions.AddHealthChecks: { + cy.byTestActionID(action) + .scrollIntoView() + .should('be.visible') + .click(); + break; + } + case 'Edit Health Checks': + case nodeActions.EditHealthChecks: { + cy.byTestActionID(action) + .scrollIntoView() + .should('be.visible') + .click(); + break; + } + default: { + throw new Error(`${action} is not available in action menu`); + } + } + }, +}; + +export const addToApplication = { + selectAction: (action: addToApplicationGroupings | string) => { + switch (action) { + // TODO (ODC-6455): Tests should use latest UI labels like "Import from Git" instead of mapping strings + case addToApplicationGroupings.FromGit: + case 'From Git': { + cy.byTestActionID(action) + .should('be.visible') + .click(); + break; + } + // TODO (ODC-6455): Tests should use latest UI labels like "Import from Git" instead of mapping strings + case addToApplicationGroupings.FromDevfile: + case 'From Devfile': { + cy.byTestActionID(action) + .should('be.visible') + .click(); + break; + } + // TODO (ODC-6455): Tests should use latest UI labels like "Import from Git" instead of mapping strings + case addToApplicationGroupings.FromDockerfile: + case 'From Dockerfile': { + cy.byTestActionID(action) + .should('be.visible') + .click(); + break; + } + case addToApplicationGroupings.ContainerImage: + case 'Container Image': { + cy.byTestActionID(action) + .should('be.visible') + .click(); + break; + } + case addToApplicationGroupings.UploadJarfile: + case 'Upload JAR file': { + cy.byTestActionID(action) + .should('be.visible') + .click(); + break; + } + case addToApplicationGroupings.EventSource: + case 'Event Source': { + cy.byTestActionID(action) + .should('be.visible') + .click(); + break; + } + case addToApplicationGroupings.Channel: + case 'Channel': { + cy.byTestActionID(action) + .should('be.visible') + .click(); + break; + } + default: { + throw new Error(`${action} is not available in menu`); + } + } + }, +}; diff --git a/integration-tests/support/pages/topology/topology-edit-deployment.ts b/integration-tests/support/pages/topology/topology-edit-deployment.ts new file mode 100644 index 0000000..26c3e1c --- /dev/null +++ b/integration-tests/support/pages/topology/topology-edit-deployment.ts @@ -0,0 +1,126 @@ +import { authenticationTypes } from '@console/dev-console/integration-tests/support/constants'; +import { topologyPO } from '@console/topology/integration-tests/support/page-objects/topology-po'; + +export const editDeployment = { + verifyModalTitle: () => { + cy.get(topologyPO.createSecret.formInputs.secretFormTitle).should('be.visible'); + }, + addSecretName: (secretName: string) => { + cy.get(topologyPO.createSecret.formInputs.secretName) + .clear() + .type(secretName); + }, + selectAuthenticationType: (authenticationType: authenticationTypes | string) => { + cy.get(topologyPO.createSecret.formInputs.authenticationType).click(); + switch (authenticationType) { + case 'Image registry credentials': + case authenticationTypes.ImageRegistryCredentials: { + cy.byTestActionID(authenticationType) + .should('be.visible') + .click(); + break; + } + case 'Upload configuration file': + case authenticationTypes.UploadConfigurationFile: { + cy.byTestActionID(authenticationType) + .should('be.visible') + .click(); + break; + } + default: { + throw new Error(`${authenticationType} is not available in action menu`); + } + } + }, + addServerAddress: (serverUrl: string) => { + cy.get(topologyPO.createSecret.formInputs.registryServerAddress) + .clear() + .type(serverUrl); + }, + enterUsername: (username: string) => { + cy.get(topologyPO.createSecret.formInputs.userName) + .clear() + .type(username); + }, + enterPassword: (password: string) => { + cy.get(topologyPO.createSecret.formInputs.password) + .clear() + .type(password); + }, + enterEmail: (email: string) => { + cy.get(topologyPO.createSecret.formInputs.email) + .clear() + .type(email); + }, + saveSecret: () => { + cy.get(topologyPO.createSecret.formInputs.saveSecret).click(); + }, + selectDeploymentStrategyType: (strategyType: string) => { + cy.get(topologyPO.deploymentStrategy.strategyTypeDropDown).click(); + + if (strategyType === 'Rolling Update') { + cy.get(topologyPO.deploymentStrategy.rollingUpdate).click(); + } else if (strategyType === 'Recreate') { + cy.get(topologyPO.deploymentStrategy.recreateStrategy).click(); + } else if (strategyType === 'Custom') { + cy.get(topologyPO.deploymentStrategy.customUpdate).click(); + } + }, + selectProjectName: (projectName: string) => { + cy.get(topologyPO.deploymentStrategy.projectDropDown).click(); + cy.get(`[id="${projectName}-link"]`).click(); + }, + selectImageStream: (imageStream: string) => { + cy.get(topologyPO.deploymentStrategy.imageStream).click(); + cy.get(`[id="${imageStream}-link"]`).click(); + }, + selectImageStreamTag: (tag: string) => { + cy.get(topologyPO.deploymentStrategy.tag).click(); + cy.get(`[id="${tag}-link"]`).click(); + }, + addPreCycleHook: (workloadName: string, failurePolicy: string) => { + cy.get(topologyPO.deploymentStrategy.preLifecycleHook.preExecNewPod).click(); + cy.get(topologyPO.deploymentStrategy.preLifecycleHook.preExecNewPodContainerDD).click(); + cy.get(`[id="${workloadName}-link"]`).click(); + cy.get(topologyPO.deploymentStrategy.preLifecycleHook.runCommand) + .clear() + .type('echo "PreLifeCycle Hook"'); + cy.get(topologyPO.deploymentStrategy.preLifecycleHook.failurePolicy).click(); + cy.get(`[id="${failurePolicy}-link"]`).click(); + cy.get(topologyPO.deploymentStrategy.tickButton).click(); + }, + selectProject: (projectName: string) => { + cy.get(`[id="${projectName}-link"]`).click(); + }, + selectImage: (imageStream: string) => { + cy.get(`[id="${imageStream}-link"]`).click(); + }, + selectImageTag: (tag: string) => { + cy.get(`[id="${tag}-link"]`).click(); + }, + addMidCycleHook: (workloadName: string, failurePolicy: string) => { + cy.get(topologyPO.deploymentStrategy.midLifecycleHook.midTagImagesField).click(); + cy.get(topologyPO.deploymentStrategy.midLifecycleHook.midTagImagesFieldContainerDD).click(); + cy.get(`[id="${workloadName}-link"]`).click(); + cy.get(topologyPO.deploymentStrategy.midLifecycleHook.projectDropDown).click(); + editDeployment.selectProject('openshift'); + cy.get(topologyPO.deploymentStrategy.midLifecycleHook.imageStream).click(); + editDeployment.selectImage('golang'); + cy.get(topologyPO.deploymentStrategy.midLifecycleHook.imageStreamTag).click(); + editDeployment.selectImageTag('latest'); + cy.get(topologyPO.deploymentStrategy.midLifecycleHook.failurePolicy).click(); + cy.get(`[id="${failurePolicy}-link"]`).click(); + cy.get(topologyPO.deploymentStrategy.tickButton).click(); + }, + addPostCycleHook: (workloadName: string, failurePolicy: string) => { + cy.get(topologyPO.deploymentStrategy.postLifecycleHook.postExecNewPod).click(); + cy.get(topologyPO.deploymentStrategy.postLifecycleHook.postExecNewPodContainerNameDD).click(); + cy.get(`[id="${workloadName}-link"]`).click(); + cy.get(topologyPO.deploymentStrategy.postLifecycleHook.runCommand) + .clear() + .type('echo "PostLifeCycle Hook"'); + cy.get(topologyPO.deploymentStrategy.postLifecycleHook.failurePolicy).click(); + cy.get(`[id="${failurePolicy}-link"]`).click(); + cy.get(topologyPO.deploymentStrategy.tickButton).click(); + }, +}; diff --git a/integration-tests/support/pages/topology/topology-helper-page.ts b/integration-tests/support/pages/topology/topology-helper-page.ts new file mode 100644 index 0000000..2a01322 --- /dev/null +++ b/integration-tests/support/pages/topology/topology-helper-page.ts @@ -0,0 +1,32 @@ +import { app } from '@console/dev-console/integration-tests/support/pages'; +import { topologyPO } from '@console/topology/integration-tests/support/page-objects/topology-po'; + +export const topologyHelper = { + search: (name: string) => + cy + .get(topologyPO.search) + .clear() + .type(name), + verifyWorkloadInTopologyPage: (appName: string, options?: { timeout: number }) => { + topologyHelper.search(appName); + // eslint-disable-next-line promise/catch-or-return + cy.get('body').then(($body) => { + if ( + $body.find('[data-test-id="topology-switcher-view"][aria-label="Graph view"]').length !== 0 + ) { + cy.get(topologyPO.list.switcher).click(); + cy.log('user is switching to graph view in topology page'); + } else { + cy.log('You are on Topology page - Graph view'); + } + }); + cy.get(topologyPO.graph.reset).click(); + cy.get(topologyPO.graph.fitToScreen).click(); + cy.get(topologyPO.highlightNode, options).should('be.visible'); + app.waitForDocumentLoad(); + }, + verifyWorkloadDeleted: (workloadName: string, options?: { timeout: number }) => { + topologyHelper.search(workloadName); + cy.get(topologyPO.highlightNode, options).should('not.exist'); + }, +}; diff --git a/integration-tests/support/pages/topology/topology-page.ts b/integration-tests/support/pages/topology/topology-page.ts new file mode 100644 index 0000000..4497136 --- /dev/null +++ b/integration-tests/support/pages/topology/topology-page.ts @@ -0,0 +1,512 @@ +import { guidedTour } from '@console/cypress-integration-tests/views/guided-tour'; +import { + devNavigationMenu, + displayOptions, + nodeActions, + sideBarTabs, +} from '@console/dev-console/integration-tests/support/constants'; +import { topologyPO } from '@console/dev-console/integration-tests/support/pageObjects'; +import { + createHelmRelease, + app, + createForm, + navigateTo, +} from '@console/dev-console/integration-tests/support/pages'; +import { gitPage } from '@console/dev-console/integration-tests/support/pages/add-flow'; +import { topologyHelper } from './topology-helper-page'; + +export const topologyPage = { + verifyUserIsInGraphView: () => { + cy.byLegacyTestID('topology-view-shortcuts').should('be.visible'); + // eslint-disable-next-line promise/catch-or-return + cy.get('body').then(($body) => { + if ($body.find('.odc-topology-list-view').length !== 0) { + cy.get(topologyPO.switcher) + .should('be.enabled') + .click({ force: true }); + cy.get(topologyPO.graph.fitToScreen).should('be.visible'); + } + }); + }, + verifyUserIsInListView: () => { + cy.byLegacyTestID('topology-view-shortcuts').should('be.visible'); + // eslint-disable-next-line promise/catch-or-return + cy.get('body').then(($body) => { + if ($body.find('.odc-topology-graph-view').length !== 0) { + cy.get(topologyPO.switcher) + .should('be.enabled') + .click({ force: true }); + cy.get(topologyPO.list.switchGraph).should('be.visible'); + } + }); + }, + waitForLoad: (timeout = 50000) => { + app.waitForLoad(); + cy.get('.loading-box.loading-box__loaded', { timeout }).should('exist'); + cy.get('[data-surface="true"]').should('be.visible'); + }, + verifyTitle: () => { + cy.get(topologyPO.title).should('have.text', 'Topology'); + }, + verifyTopologyPage: () => { + app.waitForDocumentLoad(); + cy.url().should('include', 'topology'); + }, + verifyTopologyGraphView: () => { + // eslint-disable-next-line promise/catch-or-return + cy.url().then(($text) => { + $text.includes('graph') + ? cy.log(`user is at topology graph view`) + : cy.get(topologyPO.switcher).click({ force: true }); + }); + }, + verifyContextMenu: () => cy.get(topologyPO.graph.contextMenu).should('be.visible'), + verifyNoWorkLoadsText: (text: string) => + cy.get('h3.pf-c-title.pf-m-lg').should('contain.text', text), + verifyWorkLoads: () => cy.get(topologyPO.graph.workloads).should('be.visible'), + search: (name: string) => { + topologyHelper.search(name); + }, + verifyWorkloadInTopologyPage: (appName: string) => { + topologyHelper.verifyWorkloadInTopologyPage(appName); + }, + verifyWorkloadNotInTopologyPage: (appName: string, options?: { timeout: number }) => { + topologyHelper.verifyWorkloadDeleted(appName, options); + }, + clickDisplayOptionDropdown: () => + cy + .get('.odc-topology-filter-dropdown__select') + .contains('Display options') + .click(), + checkConnectivityMode: () => cy.get(topologyPO.graph.displayOptions.connectivityMode).click(), + checkConsumptionMode: () => cy.get(topologyPO.graph.displayOptions.consumptionMode).click(), + verifyConnectivityModeChecked: () => + cy.get(topologyPO.graph.displayOptions.connectivityMode).should('be.checked'), + verifyConsumptionModeChecked: () => + cy.get(topologyPO.graph.displayOptions.consumptionMode).should('be.checked'), + verifyExpandChecked: () => + cy.get(topologyPO.graph.displayOptions.expandSwitchToggle).should('be.checked'), + verifyExpandDisabled: () => + cy.get(topologyPO.graph.displayOptions.expandSwitchToggle).should('be.disabled'), + verifyExpandOptionsDisabled: () => + cy.get(topologyPO.graph.displayOptions.applicationGroupings).should('be.disabled'), + uncheckExpandToggle: () => { + cy.get(topologyPO.graph.displayOptions.expandSwitchToggle).click({ force: true }); + }, + defaultState: () => { + // By Default: Graph View + topologyPage.verifyTopologyGraphView(); + + // eslint-disable-next-line promise/catch-or-return + cy.get('body').then((el) => { + if (el.find(topologyPO.displayFilter.applicationGroupingOption).length === 0) { + cy.get(topologyPO.displayFilter.display).click(); + } + }); + + // By Default: Expand Enabled + // eslint-disable-next-line promise/catch-or-return + cy.get(topologyPO.displayFilter.expandOption) + .as('radiobutton') + .invoke('is', ':checked') + .then((initial) => { + if (!initial) { + cy.get('@radiobutton').check({ force: true }); + } + }); + + // By Default: ApplicationGroupings Checked + // eslint-disable-next-line promise/catch-or-return + cy.get(topologyPO.displayFilter.applicationGroupingOption) + .as('checkbox') + .invoke('is', ':checked') + .then((initial) => { + if (!initial) { + cy.get('@checkbox').check({ force: true }); + } + }); + + // By Default: PodCount Unchecked + // eslint-disable-next-line promise/catch-or-return + cy.get(topologyPO.displayFilter.podLabelOptions) + .eq(2) + .as('checkbox') + .invoke('is', ':checked') + .then((initial) => { + if (initial) { + cy.get('@checkbox').uncheck({ force: true }); + } + }); + + // By Default: Labels Checked + // eslint-disable-next-line promise/catch-or-return + cy.get(topologyPO.displayFilter.podLabelOptions) + .eq(3) + .as('checkbox') + .invoke('is', ':checked') + .then((initial) => { + if (!initial) { + cy.get('@checkbox').check({ force: true }); + } + }); + }, + verifyPodCountUnchecked: () => cy.get(topologyPO.sidePane.showPodCount).should('not.be.checked'), + selectDisplayOption: (opt: displayOptions) => { + topologyPage.clickDisplayOptionDropdown(); + switch (opt) { + case displayOptions.PodCount: + cy.get('[id$=show-pod-count]').check(); + break; + case displayOptions.Labels: + cy.get('[id$=show-labels]').check(); + break; + case displayOptions.ApplicationGroupings: + cy.get('[id$=expand-app-groups]').check(); + break; + case displayOptions.HelmReleases: + cy.get('[id$=helmGrouping]').check(); + break; + case displayOptions.KnativeServices: + cy.get('[id$=knativeServices]').check(); + break; + default: + throw new Error('Option is not available'); + break; + } + }, + filterByResource: (resourceName: string) => { + cy.get(topologyPO.graph.filterDropdown) + .contains('Filter by Resource') + .click(); + cy.get(`[id$="${resourceName}"]`).check(); + }, + verifyPipelineRunStatus: (status: string) => + cy + .get('li.list-group-item.pipeline-overview') + .next('li') + .find('span.co-icon-and-text span') + .should('have.text', status), + searchHelmRelease: (name: string) => { + topologyHelper.search(name); + // eslint-disable-next-line promise/catch-or-return + cy.get('[data-kind="node"]').then(($el) => { + if ($el.find(topologyPO.highlightNode).length === 0) { + createHelmRelease(name); + } else { + cy.log('Helm Release is already available'); + } + topologyPage.verifyWorkloadInTopologyPage(name); + }); + }, + verifyHelmReleaseSidePaneTabs: () => { + cy.get(topologyPO.sidePane.tabName) + .eq(0) + .should('contain.text', sideBarTabs.Details); + cy.get(topologyPO.sidePane.tabName) + .eq(1) + .should('contain.text', sideBarTabs.Resources); + cy.get(topologyPO.sidePane.tabs) + .eq(2) + .should('contain.text', sideBarTabs.ReleaseNotes); + }, + getAppNode: (appName: string) => { + return cy.get(`[data-id="group:${appName}"] g.odc-resource-icon text`).contains('A'); + }, + getRoute: (nodeName: string) => { + return cy + .get('[data-test-id="base-node-handler"] > text') + .contains(nodeName) + .parentsUntil(topologyPO.graph.node) + .next('a') + .eq(2); + }, + getBuild: (nodeName: string) => { + return cy.get(`a[href="/k8s/ns/aut/builds/${nodeName}-1/logs"]`); + }, + componentNode: (nodeName: string) => { + return cy.get('g.pf-topology__node__label > text').contains(nodeName); + }, + componentNodeClick: (nodeName: string, options?: { timeout: number }) => { + topologyHelper.search(nodeName); + cy.get('[data-type="workload"] .is-filtered [data-test-id="base-node-handler"]', options) + .first() + .click({ force: true }); + }, + knativeNode: (nodeName: string) => { + return cy.get('g.odc-knative-service__label > text').contains(nodeName); + }, + getEventSource: (eventSource: string) => { + return cy + .get('[data-type="event-source"] g.pf-topology__node__label > text') + .contains(eventSource); + }, + getRevisionNode: (serviceName: string) => { + cy.get('[data-type="knative-service"] g.pf-topology__group__label > text', { timeout: 80000 }) + .contains(serviceName) + .should('be.visible'); + return cy.get('[data-type="knative-revision"] ellipse.pf-topology__node__background'); + }, + verifyContextMenuOptions: (...options: string[]) => { + cy.get('#popper-container li[role="menuitem"]').each(($el) => { + expect(options).toContain($el.text()); + }); + }, + verifyDecorators: (nodeName: string, numOfDecorators: number) => + topologyPage + .componentNode(nodeName) + .siblings('a') + .should('have.length', numOfDecorators), + selectContextMenuAction: (action: nodeActions | string) => { + cy.byTestActionID(action) + .should('be.visible') + .click(); + }, + getNode: (nodeName: string) => { + return cy + .get(topologyPO.graph.nodeLabel) + .should('be.visible') + .contains(nodeName); + }, + getNodeLabel: (nodeName: string) => { + return cy + .get(topologyPO.graph.selectNodeLabel) + .should('be.visible') + .contains(nodeName); + }, + getKnativeNode: (nodeName: string) => { + return cy + .get(topologyPO.graph.knativeNodeLabel) + .should('be.visible') + .contains(nodeName); + }, + getGroup: (groupName: string) => { + return cy + .get(topologyPO.graph.groupLabel) + .should('be.visible') + .contains(groupName); + }, + getDeploymentNode: (nodeName: string) => { + return cy + .get(topologyPO.graph.nodeLabel) + .should('be.visible') + .contains(new RegExp(`Deployment.*${nodeName}`)); + }, + rightClickOnNode: (nodeName: string) => { + topologyPage.getNode(nodeName).trigger('contextmenu', { force: true }); + }, + rightClickOnKnativeNode: (nodeName: string) => { + topologyPage.getKnativeNode(nodeName).trigger('contextmenu', { force: true }); + }, + rightClickOnGroup: (releaseName: string) => { + topologyPage.getGroup(releaseName).trigger('contextmenu', { force: true }); + }, + rightClickOnApplicationGroupings: (appName: string) => { + const id = `[data-id="group:${appName}"]`; + cy.get(id) + .should('be.visible') + .first() + .trigger('contextmenu', { force: true }); + }, + clickOnNode: (nodeName: string) => { + topologyPage.getNode(nodeName).click({ force: true }); + }, + clickOnNodeLabel: (nodeName: string) => { + topologyPage.getNodeLabel(nodeName).click({ force: true }); + }, + clickOnGroup: (groupName: string) => { + topologyPage.getGroup(groupName).click({ force: true }); + }, + clickOnDeploymentNode: (nodeName: string) => { + topologyPage.getDeploymentNode(nodeName).click(); + }, + clickOnApplicationGroupings: (appName: string) => { + const id = `[data-id="group:${appName}"] .odc-resource-icon-application`; + cy.get(id) + .next('text') + .click({ force: true }); + }, + verifyApplicationGroupingsDeleted: (appName: string) => { + cy.reload(); + app.waitForLoad(); + guidedTour.close(); + const id = `[data-id="group:${appName}"]`; + cy.get(id, { timeout: 50000 }).should('not.exist'); + }, + verifyApplicationGroupings: (workloadName: string) => { + cy.get(topologyPO.sidePane.applicationGroupingsTitle).should('be.visible'); + cy.byLegacyTestID(workloadName).should('be.visible'); + }, + clickOnSinkBinding: (nodeName: string = 'sink-binding') => { + topologyPage.getNode(nodeName).click({ force: true }); + }, + getKnativeService: (serviceName: string) => { + return cy + .get('[data-type="knative-service"]') + .find(topologyPO.graph.groupLabel) + .contains(serviceName); + }, + getKnativeRevision: (serviceName: string) => { + return cy + .get('[data-type="knative-revision"]') + .find('g.odc-workload-node') + .contains(serviceName); + }, + waitForKnativeRevision: () => { + cy.get(topologyPO.graph.node, { timeout: 300000 }).should('be.visible'); + }, + rightClickOnHelmWorkload: () => { + cy.get(topologyPO.graph.node) + .find('circle') + .trigger('contextmenu', { force: true }); + }, + clickOnHelmWorkload: () => { + cy.get(topologyPO.graph.node) + .find('circle') + .click({ force: true }); + }, + clickWorkloadUrl: (workloadName: string) => { + cy.get('[data-type="workload"] text') + .contains(workloadName) + .parentsUntil(topologyPO.graph.node) + .siblings('a') + .first() + .click({ force: true }); + }, + clickOnKnativeService: (knativeService: string) => { + topologyPage.getKnativeService(knativeService).click({ force: true }); + }, + rightClickOnKnativeService: (knativeService: string) => { + topologyPage.getKnativeService(knativeService).trigger('contextmenu', { force: true }); + }, + addStorage: { + pvc: { + clickUseExistingClaim: () => { + cy.get(topologyPO.addStorage.pvc.useExistingClaim).check(); + }, + createNewClaim: { + clickCreateNewClaim: () => { + cy.get(topologyPO.addStorage.pvc.createNewClaim.newClaim).check(); + }, + selectStorageClass: (storageClass: string = 'standard') => { + cy.get(topologyPO.addStorage.pvc.createNewClaim.storageClass).click(); + cy.byLegacyTestID('dropdown-text-filter').type(storageClass); + cy.get('ul[role="listbox"]') + .find('li') + .contains(storageClass) + .click(); + }, + enterPVCName: (name: string) => { + cy.get(topologyPO.addStorage.pvc.createNewClaim.pvcName).type(name); + }, + enterSize: (size: string) => { + cy.get(topologyPO.addStorage.pvc.createNewClaim.accessMode.size).type(size); + }, + }, + }, + enterMountPath: (mountPath: string) => { + cy.get(topologyPO.addStorage.mountPath).type(mountPath); + }, + clickSave: () => { + cy.get(topologyPO.addStorage.save).click(); + }, + }, + revisionDetails: { + clickOnDetailsTab: () => cy.get(topologyPO.revisionDetails.detailsTab).click(), + clickOnYAMLTab: () => cy.get(topologyPO.revisionDetails.yamlTab).click(), + details: { + verifyRevisionSummary: () => + cy.get(topologyPO.revisionDetails.details.resourceSummary).should('be.visible'), + verifyConditionsSection: () => + cy.get(topologyPO.revisionDetails.details.conditionsTitle).should('be.visible'), + }, + yaml: { + clickOnSave: () => cy.get(topologyPO.revisionDetails.yaml.save).click(), + }, + }, + verifyRunTimeIconForContainerImage: (runTimeIcon: string) => { + cy.get('[data-type="workload"] .is-filtered [data-test-id="base-node-handler"]') + .find('image') + .should('have.attr', 'xlink:href') + .and('include', runTimeIcon); + }, + deleteApplication: (appName: string) => { + cy.get(topologyPO.graph.deleteApplication) + .clear() + .type(appName); + cy.get(topologyPO.graph.deleteWorkload).click(); + cy.wait(15000); + }, + verifyApplicationGroupingSidepane: () => { + cy.get(topologyPO.sidePane.applicationGroupingsTitle).should('be.visible'); + cy.get(topologyPO.sidePane.resourcesTabApplicationGroupings).should('be.visible'); + }, + startBuild: () => { + cy.get('button[data-test-id="start-build-action"]') + .should('be.visible') + .click({ force: true }); + }, + verifyNodeAlert: (nodeName: string) => { + cy.get('[data-type="workload"]') + .find('.pf-topology__node.pf-m-warning') + .contains(nodeName); + }, + verifyListNodeAlert: (nodeName: string) => { + cy.get('.odc-topology-list-view__label-cell') + .contains(nodeName) + .parent() + .find('div.odc-topology-list-view__alert-cell') + .contains('Alerts:'); + }, + clickMaxZoomOut: () => { + cy.get(topologyPO.graph.emptyGraph).click(); + cy.get(topologyPO.graph.reset).click(); + for (let i = 0; i < 5; i++) { + cy.get(topologyPO.graph.zoomOut).click(); + } + }, +}; + +export const addGitWorkload = ( + gitUrl: string = 'https://github.com/sclorg/nodejs-ex.git', + componentName: string = 'nodejs-ex-git', + resourceType: string = 'Deployment', +) => { + gitPage.enterGitUrl(gitUrl); + gitPage.verifyValidatedMessage(gitUrl); + gitPage.enterComponentName(componentName); + gitPage.selectResource(resourceType); + createForm.clickCreate(); + app.waitForLoad(); +}; + +export const topologyListPage = { + clickOnApplicationGroupings: (appName: string) => { + const id = `[data-test="group:${appName}"]`; + cy.get(id).click({ force: true }); + }, +}; + +export const createServiceBindingConnect = ( + bindingName: string = 'testing', + senderNode: string, + recieverNode: string, +) => { + topologyPage.rightClickOnNode(senderNode); + cy.byTestActionID('Create Service Binding') + .should('be.visible') + .click(); + cy.get('#form-input-name-field') + .should('be.visible') + .clear() + .type(bindingName); + cy.get('#form-ns-dropdown-service-field') + .should('be.visible') + .click(); + cy.get(`#${recieverNode}-link`) + .should('be.visible') + .click(); + cy.get('#confirm-action').click(); + navigateTo(devNavigationMenu.Add); + navigateTo(devNavigationMenu.Topology); + cy.get('[data-test-id="edge-handler"]', { timeout: 15000 }).should('be.visible'); +}; diff --git a/integration-tests/support/pages/topology/topology-side-pane-page.ts b/integration-tests/support/pages/topology/topology-side-pane-page.ts new file mode 100644 index 0000000..d32a8d6 --- /dev/null +++ b/integration-tests/support/pages/topology/topology-side-pane-page.ts @@ -0,0 +1,174 @@ +import { nodeActions, resources } from '@console/dev-console/integration-tests/support/constants'; +import { topologyPO } from '@console/dev-console/integration-tests/support/pageObjects'; +import { app } from '@console/dev-console/integration-tests/support/pages'; +import { topologyActions } from './topology-actions-page'; + +export const topologySidePane = { + verify: () => cy.get(topologyPO.sidePane.dialog).should('be.visible'), + verifyTitle: (nodeName: string) => cy.get(topologyPO.sidePane.title).should('contain', nodeName), + verifySelectedTab: (tabName: string) => + cy + .get(topologyPO.sidePane.tabName) + .contains(tabName) + .parent('li') + .should('have.class', 'co-m-horizontal-nav-item--active'), + verifyTab: (tabName: string) => + cy + .get(topologyPO.sidePane.tabName) + .contains(tabName) + .should('be.visible'), + verifyTabNotVisible: (tabName: string) => + cy + .get(topologyPO.sidePane.tabName) + .contains(tabName) + .should('not.be.visible'), + verifyActionsDropDown: () => cy.get(topologyPO.sidePane.actionsDropDown).should('be.visible'), + clickActionsDropDown: () => cy.get(topologyPO.sidePane.actionsDropDown).click(), + selectTab: (tabName: string) => { + app.waitForLoad(160000, true); + cy.get(topologyPO.sidePane.tabName) + .contains(tabName) + .click({ force: true }); + }, + verifySection: (sectionTitle: string) => { + cy.get(topologyPO.sidePane.dialog).within(() => { + cy.contains(topologyPO.sidePane.sectionTitle, sectionTitle).should('be.visible'); + }); + }, + verifyActions: (...actions: string[]) => { + cy.byLegacyTestID('action-items') + .find('li') + .each(($el) => { + expect(actions).toContain($el.text()); + }); + }, + close: () => + cy + .get(topologyPO.sidePane.close) + .scrollIntoView() + .click(), + verifyFieldInDetailsTab: (fieldName: string) => + cy + .get(`[data-test-selector="details-item-label__${fieldName}"]`) + .scrollIntoView() + .should('be.visible'), + verifyWorkload: () => + cy + .get(topologyPO.sidePane.sectionTitle) + .contains('Services') + .next('ul li a') + .should('be.visible'), + verifyFieldValue: (fieldName: string, fieldValue: string) => + cy + .get(`[data-test-selector="details-item-value__${fieldName}"]`) + .should('contain.text', fieldValue), + selectAddHealthChecks: () => + cy + .get('a') + .contains('Add Health Checks') + .click(), + scaleUpPodCount: () => + cy + .get(topologyPO.sidePane.podScale) + .eq(0) + .click(), + scaleDownPodCount: () => + cy + .get(topologyPO.sidePane.podScale) + .eq(1) + .click(), + verifyPodText: (scaleNumber: string) => { + cy.get(topologyPO.sidePane.podText, { timeout: 120000 }).should('contain.text', scaleNumber); + }, + verifyHealthCheckAlert: () => cy.get(topologyPO.sidePane.healthCheckAlert).should('be.visible'), + verifyResourceQuotaAlert: () => + cy.get(topologyPO.sidePane.resourceQuotaAlert).should('be.visible'), + verifyWorkloadInAppSideBar: (workloadName: string) => + cy + .get(topologyPO.sidePane.dialog) + .find('a') + .should('contain.text', workloadName), + selectNodeAction: (action: nodeActions | string) => { + cy.byLegacyTestID('actions-menu-button').click(); + topologyActions.selectAction(action); + }, + verifyLabel: (labelName: string, timeout = 80000) => { + cy.get(topologyPO.sidePane.detailsTab.labels) + .contains(labelName, { timeout }) + .should('be.visible'); + }, + verifyAnnotation: (annotationName: string) => { + cy.byTestID('edit-annotations').click(); + cy.byTestID('label-list') + .find('a') + .contains(annotationName, { timeout: 80000 }) + .should('be.visible'); + }, + verifyNumberOfAnnotations: (num: string) => { + cy.wait(3000); + cy.get(topologyPO.sidePane.detailsTab.annotations) + .scrollIntoView() + .should('be.visible'); + // eslint-disable-next-line promise/catch-or-return + cy.get(topologyPO.sidePane.editAnnotations).then(($el) => { + const res = $el.text().split(' '); + expect(res[0]).toEqual(num); + }); + }, + verifyResource: (resourceName: string) => { + topologySidePane.selectTab('Resources'); + cy.byLegacyTestID(resourceName).should('be.visible'); + }, + clickStartLastRun: () => { + cy.get(topologyPO.sidePane.resourcesTab.startLastRun) + .should('be.enabled') + .click(); + }, + verifyPipelineRuns: () => { + cy.get(topologyPO.sidePane.resourcesTab.pipelineRuns).should('be.visible'); + }, + verifyResourcesApplication: (deploymentName: string) => { + cy.byTestID(deploymentName).should('be.visible'); + }, + verifyActionsOnApplication: () => { + cy.byTestActionID('Delete application').should('be.visible'); + cy.get(topologyPO.addToApplicationInContext).should('be.visible'); + }, + selectResource: (opt: resources | string, namespace: string, name: string) => { + switch (opt) { + case 'Deployments': + case resources.Deployments: { + cy.get(`[href="/k8s/ns/${namespace}/deployments/${name}"]`).click(); + break; + } + case 'Build Configs': + case resources.BuildConfigs: { + cy.get(`[href="/k8s/ns/${namespace}/buildconfigs/${name}"]`).click(); + break; + } + case 'Builds': + case resources.Builds: { + cy.get(`[href="/k8s/ns/${namespace}/builds/${name}"]`).click(); + break; + } + case 'Services': + case resources.Services: { + cy.get(`[href="/k8s/ns/${namespace}/services/${name}"]`).click(); + break; + } + case 'Image Streams': + case resources.ImageStreams: { + cy.get(`[href="/k8s/ns/${namespace}/imagestreams/${name}"]`).click(); + break; + } + case 'Routes': + case resources.Routes: { + cy.get(`[href="/k8s/ns/${namespace}/routes/${name}"]`).click(); + break; + } + default: { + throw new Error('resource is not available'); + } + } + }, +}; diff --git a/integration-tests/support/step-definitions/common/topology.ts b/integration-tests/support/step-definitions/common/topology.ts new file mode 100644 index 0000000..be5796a --- /dev/null +++ b/integration-tests/support/step-definitions/common/topology.ts @@ -0,0 +1,221 @@ +import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps'; +import { guidedTour } from '@console/cypress-integration-tests/views/guided-tour'; +import { nav } from '@console/cypress-integration-tests/views/nav'; +import { + switchPerspective, + devNavigationMenu, +} from '@console/dev-console/integration-tests/support/constants/global'; +import { + createGitWorkload, + gitPage, + topologyHelper, +} from '@console/dev-console/integration-tests/support/pages'; +import { + perspective, + projectNameSpace, + navigateTo, + createForm, +} from '@console/dev-console/integration-tests/support/pages/app'; +import { + verifyAndInstallGitopsPrimerOperator, + verifyAndInstallKnativeOperator, + verifyAndInstallPipelinesOperator, +} from '@console/dev-console/integration-tests/support/pages/functions/installOperatorOnCluster'; +import { topologyPO } from '@console/topology/integration-tests/support/page-objects/topology-po'; +import { topologyPage } from '@console/topology/integration-tests/support/pages/topology/topology-page'; +import { topologySidePane } from '@console/topology/integration-tests/support/pages/topology/topology-side-pane-page'; + +Given('user is at the Topology page', () => { + navigateTo(devNavigationMenu.Topology); + topologyPage.verifyTopologyPage(); +}); + +Given('user is at Topology page', () => { + navigateTo(devNavigationMenu.Topology); + topologyPage.verifyTopologyPage(); +}); + +When('user navigates to Topology page', () => { + navigateTo(devNavigationMenu.Topology); +}); + +Then('user is able to see workload {string} in topology page list view', (workloadName: string) => { + topologyPage.verifyWorkloadInTopologyPage(workloadName); +}); + +Then('user is able to see workload {string} in topology page', (workloadName: string) => { + topologyPage.verifyWorkloadInTopologyPage(workloadName); +}); + +Then('user will be redirected to Topology page', () => { + topologyPage.verifyTopologyPage(); +}); + +When('user clicks on workload {string}', (workloadName: string) => { + topologyPage.componentNode(workloadName).click({ force: true }); +}); + +Then('user can see sidebar opens with Resources tab selected by default', () => { + topologySidePane.verifySelectedTab('Resources'); +}); + +Then('side bar is displayed with the pipelines section', () => { + topologySidePane.verifyTab('Resources'); + topologySidePane.verifySection('Pipeline Runs'); +}); + +Then('user can see sidebar Details, Resources and Observe tabs', () => { + topologySidePane.verifyTab('Details'); + topologySidePane.verifyTab('Resources'); + topologySidePane.verifyTab('Observe'); +}); + +Then('user can see sidebar Details, Resources tabs', () => { + topologySidePane.verifyTab('Details'); + topologySidePane.verifyTab('Resources'); +}); + +When('user goes to Details tab', () => { + topologySidePane.selectTab('Details'); +}); + +Then('user can see close button', () => { + topologySidePane.close(); +}); + +Then( + 'user verifies name of the node {string} and Action drop down present on top of the sidebar', + (nodeName: string) => { + topologySidePane.verifyTitle(nodeName); + topologySidePane.verifyActionsDropDown(); + }, +); + +Then('user is able to see health check notification', () => { + topologySidePane.verifyHealthCheckAlert(); +}); + +Given('user is at developer perspective', () => { + perspective.switchTo(switchPerspective.Developer); + guidedTour.close(); + nav.sidenav.switcher.shouldHaveText(switchPerspective.Developer); +}); + +Given('user has created namespace starts with {string}', (projectName: string) => { + const d = new Date(); + const timestamp = d.getTime(); + projectNameSpace.selectOrCreateProject(`${projectName}-${timestamp}-ns`); +}); + +Given('user has created or selected namespace {string}', (projectName: string) => { + Cypress.env('NAMESPACE', projectName); + projectNameSpace.selectOrCreateProject(`${projectName}`); +}); + +Given( + 'user has created workload {string} with resource type {string}', + (componentName: string, resourceType: string = 'Deployment') => { + createGitWorkload( + 'https://github.com/sclorg/nodejs-ex.git', + componentName, + resourceType, + 'nodejs-ex-git-app', + ); + topologyHelper.verifyWorkloadInTopologyPage(componentName); + }, +); + +Given( + 'user has created workload {string} with resource type {string} and application groupings {string}', + (componentName: string, resourceType: string = 'Deployment', applicationGroupings: string) => { + createGitWorkload( + 'https://github.com/sclorg/nodejs-ex.git', + componentName, + resourceType, + applicationGroupings, + ); + topologyPage.verifyWorkloadInTopologyPage(componentName); + }, +); + +Given('user is at Add page', () => { + navigateTo(devNavigationMenu.Add); +}); + +When( + 'user right clicks on the workload {string} to open the Context Menu', + (workloadName: string) => { + topologyPage.rightClickOnNode(workloadName); + }, +); + +Then('user will see workload disappeared from topology', () => { + navigateTo(devNavigationMenu.Add); + navigateTo(devNavigationMenu.Topology); + cy.get(topologyPO.emptyStateIcon).should('be.visible'); +}); + +Given('user has installed OpenShift Serverless Operator', () => { + verifyAndInstallKnativeOperator(); +}); + +Given('user is at Topology Graph view', () => { + topologyPage.verifyTopologyGraphView(); +}); + +When('user clicks Start building your application', () => { + cy.get(topologyPO.emptyView.startBuildingYourApplicationLink).click(); +}); + +When('user enters {string} builder image in Quick Search bar', (searchItem: string) => { + cy.get(topologyPO.quickSearch).type(searchItem); + cy.byTestID('item-name-.NET-Builder Images') + .first() + .click(); +}); + +When('user clicks Create application on Quick Search Dialog', () => { + cy.get('.pf-c-spinner__tail-ball').should('not.exist'); + cy.get('ul[aria-label="Quick search list"] li') + .contains('Builder Images', { timeout: 60000 }) + .click(); + cy.get('button') + .contains('Create') + .click(); +}); + +When( + 'user enters Git Repo URL as {string} in Create Source-to-Image Application', + (gitUrl: string) => { + gitPage.enterGitUrl(gitUrl); + cy.get('#form-input-git-url-field-helper').should('have.text', 'Validated'); + }, +); + +When('user clicks Create button on Create Source-to-Image Application page', () => { + createForm.clickCreate(); +}); + +When('user enters Application Name as {string}', (appName: string) => { + gitPage.enterAppName(appName); +}); + +When('user enters Name as {string}', (name: string) => { + gitPage.enterWorkloadName(name); +}); + +When('user selects {string} in Resource type section', (resourceType: string) => { + gitPage.selectResource(resourceType); +}); + +Given('user has installed Gitops primer Operator', () => { + verifyAndInstallGitopsPrimerOperator(); +}); + +Given('user has installed OpenShift Pipelines Operator', () => { + verifyAndInstallPipelinesOperator(); +}); + +When('user selects Resource as deployment', () => { + cy.byTestID('kubernetes-view-input').click(); +}); diff --git a/integration-tests/support/step-definitions/export-application/export-of-application.ts b/integration-tests/support/step-definitions/export-application/export-of-application.ts new file mode 100644 index 0000000..9edb0a0 --- /dev/null +++ b/integration-tests/support/step-definitions/export-application/export-of-application.ts @@ -0,0 +1,112 @@ +import { When, Then, Given } from 'cypress-cucumber-preprocessor/steps'; +import { devNavigationMenu } from '@console/dev-console/integration-tests/support/constants/global'; +import { createGitWorkloadIfNotExistsOnTopologyPage } from '@console/dev-console/integration-tests/support/pages'; +import { + app, + navigateTo, + projectNameSpace, +} from '@console/dev-console/integration-tests/support/pages/app'; +import { + exportApplication, + exportModalButton, + closeExportNotification, +} from '../../page-objects/export-applications-po'; +import { topologyPO } from '../../page-objects/topology-po'; +import { topologyHelper } from '../../pages/topology'; + +Given( + 'user has created {string} workload in {string} application', + (nodeName: string, appName: string) => { + createGitWorkloadIfNotExistsOnTopologyPage( + 'https://github.com/sclorg/nodejs-ex.git', + nodeName, + 'Deployment', + appName, + ); + topologyHelper.verifyWorkloadInTopologyPage(nodeName); + }, +); +When('user navigates to Topology page', () => { + navigateTo(devNavigationMenu.Topology); +}); + +When('user clicks on Export Application button', () => { + cy.get(exportApplication.resourceAddedNotification).should('not.exist'); + cy.get(exportApplication.exportApplicationButton) + .should('be.visible') + .click(); +}); + +When('user clicks on Ok button on Export Application modal to start the export', () => { + cy.get('.modal-body').contains('Do you want to export your application?'); + cy.byTestID('close-btn') + .should('be.visible') + .click(); +}); + +Then('user can see a toast message saying {string}', (message: string) => { + cy.get(exportApplication.infoTip, { timeout: 5000 }).should('include.text', message); + closeExportNotification(); +}); + +Then( + 'user can see a toast message saying {string} with download option and close button', + (message: string) => { + cy.get(exportApplication.infoTip, { timeout: 180000 }).should('include.text', message); + cy.byTestID('download-export').contains('Download'); + closeExportNotification(); + }, +); + +Then('user can see primer deployment created in topology', () => { + topologyHelper.verifyWorkloadInTopologyPage('primer', { timeout: 120000 }); +}); + +Given('user is at Topology page', () => { + navigateTo(devNavigationMenu.Topology); +}); + +When('user clicks on Export Application button again', () => { + cy.get(exportApplication.exportApplicationButton) + .should('be.visible') + .click(); + cy.get(exportApplication.exportView, { timeout: 50000 }).should('be.visible'); +}); + +Then( + 'user can see {string} link, {string}, {string}, and {string} button', + (el1, el2, el3, el4) => { + cy.get(exportModalButton(el1)).should('be.visible'); + cy.get(exportModalButton(el2)).should('be.visible'); + cy.get(exportModalButton(el3)).should('be.visible'); + cy.get(exportModalButton(el4)).should('be.visible'); + cy.get(exportModalButton('Cancel Export')).click(); + cy.get(exportModalButton('Cancel Export')).should('not.exist'); + }, +); + +Given('user has created or selected namespace {string}', (componentName: string) => { + projectNameSpace.selectOrCreateProject(componentName); +}); + +Then('user can see Export Application button disabled', () => { + cy.get(exportApplication.exportApplicationButton).should('be.disabled'); +}); + +When('user clicks on Restart button', () => { + cy.get(exportModalButton('Restart Export')) + .should('be.visible') + .click(); +}); + +When('user clicks on Cancel button', () => { + cy.get(exportModalButton('Cancel Export')) + .should('be.visible') + .click(); +}); + +Given('user can see primer job gets deleted in topology', () => { + topologyHelper.search('primer'); + cy.get(topologyPO.highlightNode, { timeout: 30000 }).should('not.exist'); + app.waitForDocumentLoad(); +}); diff --git a/integration-tests/support/step-definitions/topology/application-groupings.ts b/integration-tests/support/step-definitions/topology/application-groupings.ts new file mode 100644 index 0000000..ec46ae1 --- /dev/null +++ b/integration-tests/support/step-definitions/topology/application-groupings.ts @@ -0,0 +1,120 @@ +import { When, Then, Given } from 'cypress-cucumber-preprocessor/steps'; +import { + createGitWorkloadIfNotExistsOnTopologyPage, + topologyHelper, +} from '@console/dev-console/integration-tests/support/pages'; +import { addToApplication } from '@console/topology/integration-tests/support/pages/topology/topology-actions-page'; +import { + topologyPage, + addGitWorkload, + topologyListPage, +} from '@console/topology/integration-tests/support/pages/topology/topology-page'; +import { topologySidePane } from '@console/topology/integration-tests/support/pages/topology/topology-side-pane-page'; +import { topologyPO } from '../../page-objects/topology-po'; + +When('user right clicks on application {string} to open Context Menu', (appName: string) => { + topologyPage.rightClickOnApplicationGroupings(appName); +}); + +When('user clicks on application groupings {string}', (applicationGroupings: string) => { + topologyPage.clickOnApplicationGroupings(applicationGroupings); +}); + +Then( + 'user is able to see workload {string} under resources tab in the sidebar', + (workloadName: string) => { + topologyPage.verifyApplicationGroupings(workloadName); + }, +); + +Then('user can see Actions dropdown menu', () => { + topologySidePane.verifyActionsDropDown(); +}); + +Then('user can view Add to application and Delete application options', () => { + topologySidePane.verifyActionsOnApplication(); +}); + +When( + 'user fills the form with workload name {string} and clicks Create', + (workloadName: string) => { + addGitWorkload('https://github.com/sclorg/nodejs-ex.git', workloadName, 'Deployment'); + }, +); + +When('user clicks on {string}', (addOption: string) => { + addToApplication.selectAction(addOption); +}); + +Then('user can see {string} workload', (workloadName: string) => { + topologyHelper.verifyWorkloadInTopologyPage(workloadName); +}); + +When( + 'user enters the name {string} in the Delete application modal and clicks on Delete button', + (appName: string) => { + topologyPage.deleteApplication(appName); + }, +); + +Then('user will not see Application groupings {string}', (appName: string) => { + topologyPage.verifyApplicationGroupingsDeleted(appName); +}); + +Then( + 'user can see sidebar opens with Resources tab selected by default for application groupings', + () => { + topologyPage.verifyApplicationGroupingSidepane(); + }, +); + +When('user hovers on Add to Application from action menu', () => { + cy.get(topologyPO.grouping.addToApplication).trigger('mouseover'); +}); + +When('user clicks on Import From Git option', () => { + cy.get(topologyPO.grouping.importFromGitOption).click(); +}); + +Given('user has created workload with resource type deployment', () => { + createGitWorkloadIfNotExistsOnTopologyPage( + 'https://github.com/sclorg/nodejs-ex.git', + 'ex-node-js', + 'deployment', + 'nodejs-ex-git-app', + ); + topologyHelper.verifyWorkloadInTopologyPage('ex-node-js'); +}); + +When('user clicks on the Resources dropdown', () => { + cy.get(topologyPO.grouping.filterResources).click(); +}); + +Then('user sees that all the checkboxes are unchecked', () => { + cy.get(topologyPO.grouping.deploymentCheckbox).should('not.be.checked'); +}); + +When('user clicks on list view button', () => { + topologyPage.verifyUserIsInGraphView(); + cy.get(topologyPO.quickSearchPO.listView).click(); +}); + +Then( + 'user clicks on application grouping {string} in the list view', + (applicationGrouping: string) => { + topologyListPage.clickOnApplicationGroupings(applicationGrouping); + }, +); + +Given( + 'user has created workload {string} in application grouping {string}', + (workloadName: string, appName: string) => { + createGitWorkloadIfNotExistsOnTopologyPage( + 'https://github.com/sclorg/nodejs-ex.git', + workloadName, + 'deployment', + appName, + ); + topologyHelper.verifyWorkloadInTopologyPage('ex-node-js'); + }, +); diff --git a/integration-tests/support/step-definitions/topology/chart-view.ts b/integration-tests/support/step-definitions/topology/chart-view.ts new file mode 100644 index 0000000..20dcf46 --- /dev/null +++ b/integration-tests/support/step-definitions/topology/chart-view.ts @@ -0,0 +1,491 @@ +import { When, Then, Given } from 'cypress-cucumber-preprocessor/steps'; +import { guidedTour } from '@console/cypress-integration-tests/views/guided-tour'; +import { modal } from '@console/cypress-integration-tests/views/modal'; +import { nav } from '@console/cypress-integration-tests/views/nav'; +import { pageTitle } from '@console/dev-console/integration-tests/support/constants'; +import { + devNavigationMenu, + operators, + resources, + switchPerspective, +} from '@console/dev-console/integration-tests/support/constants/global'; +import { + createGitWorkload, + createOperatorBacked, + verifyAndInstallOperator, +} from '@console/dev-console/integration-tests/support/pages'; +import { + app, + navigateTo, + perspective, + projectNameSpace, + yamlEditor, +} from '@console/dev-console/integration-tests/support/pages/app'; +import { chartAreaPO } from '../../page-objects/chart-area-po'; +import { topologyPO, typeOfWorkload } from '../../page-objects/topology-po'; +import { + addToProjectOptions, + createWorkloadUsingOptions, + verifyMultipleWorkloadInTopologyPage, +} from '../../pages/functions/chart-functions'; +import { + createServiceBindingConnect, + topologyActions, + topologyHelper, + topologyPage, + topologySidePane, +} from '../../pages/topology'; + +When('user navigates to Topology page', () => { + navigateTo(devNavigationMenu.Topology); +}); + +Then('user sees Topology page with message {string}', (message: string) => { + cy.get(topologyPO.emptyText).contains(message); +}); + +Given('user is at the Topology page', () => { + navigateTo(devNavigationMenu.Topology); +}); + +When('user clicks on {string} link in the topology page', (addLink) => { + cy.get(topologyPO.graph.addLink) + .should('be.visible') + .should('have.text', addLink); + cy.get(topologyPO.graph.addLink).click(); +}); + +Then('user will be redirected to Add page', () => { + cy.url().should('include', 'add'); + app.waitForLoad(); + cy.contains(pageTitle.Add).should('be.visible'); +}); + +When('user clicks on {string} link in the empty topology page', (build: string) => { + cy.byTestID( + build + .toLowerCase() + .replace(/\s+/g, '-') + .trim(), + ) + .should('be.visible') + .click(); + cy.log(`Quick search bar can be seen with ${build} link`); +}); + +Then('user will be able to see Add to project search bar', () => { + cy.get(topologyPO.graph.quickSearch).should('be.visible'); +}); + +Given('user has created a deployment workload named {string}', (componentName: string) => { + navigateTo(devNavigationMenu.Add); + createGitWorkload( + 'https://github.com/sclorg/nodejs-ex.git', + componentName, + 'Deployment', + 'nodejs-ex-git-app', + ); +}); + +Given('user has created a deployment workload {string}', (componentName: string) => { + navigateTo(devNavigationMenu.Add); + createGitWorkload( + 'https://github.com/sclorg/nodejs-ex.git', + componentName, + 'Deployment', + 'nodejs-ex-git-app', + ); +}); + +Given('user has created a deployment config workload {string}', (componentName: string) => { + navigateTo(devNavigationMenu.Add); + createGitWorkload( + 'https://github.com/sclorg/nodejs-ex.git', + componentName, + 'Deployment Config', + 'nodejs-ex-git-app', + ); +}); + +When('user navigates to Topology page', () => { + navigateTo(devNavigationMenu.Topology); +}); + +Then( + 'user sees {string} and {string} workloads in topology chart area', + (workload1: string, workload2: string) => { + topologyHelper.verifyWorkloadInTopologyPage(workload1); + topologyHelper.verifyWorkloadInTopologyPage(workload2); + }, +); + +Given( + 'user created {string} workload', + (resourceType: string, componentName: string = `${resourceType.toLowerCase()}-ex-git`) => { + navigateTo(devNavigationMenu.Add); + createGitWorkload( + 'https://github.com/sclorg/django-ex.git', + componentName, + resourceType, + 'django-ex-git-app', + ); + }, +); + +When('user is at Topology page chart view', () => { + navigateTo(devNavigationMenu.Topology); + cy.get(topologyPO.switcher).should('have.attr', 'aria-label', 'List view'); +}); + +When('user clicks the filter by resource on top', () => { + cy.get(topologyPO.filterByResourceDropDown) + .should('be.visible') + .click(); +}); + +When('user clicks on {string} option', (resourceType: string) => { + cy.byTestID(resourceType) + .should('be.visible') + .click(); +}); + +Then('user can see only the {string} workload', (workload: string) => { + cy.get(typeOfWorkload(workload)).should('be.visible'); +}); + +When('user selects {string} option in filter menu', (searchType: string) => { + cy.get('#toggle-id').click(); + cy.get("[role='menu']") + .find('li') + .contains(searchType) + .should('be.visible') + .click(); +}); + +When('user searches for label {string}', (search: string) => { + cy.get(topologyPO.search) + .clear() + .type(search); + cy.get('.co-suggestion-box__suggestions') + .find('button') + .contains(search) + .should('be.visible') + .first() + .click(); +}); + +Then('user can see the workload {string} visible', (workload: string) => { + cy.get(topologyPO.highlightNode).should('be.visible'); + cy.get(topologyPO.highlightNode).should('contain', workload); + app.waitForDocumentLoad(); +}); + +When('user clicks on workload {string} to open sidebar', (workloadName: string) => { + topologyPage.componentNode(workloadName).click({ force: true }); +}); + +When( + 'user opens the details page for {string} by clicking on the title', + (workloadName: string) => { + topologySidePane.selectResource( + resources.Deployments, + 'aut-topology-delete-workload', + `${workloadName}`, + ); + }, +); + +When('user navigate back to Topology page', () => { + navigateTo(devNavigationMenu.Topology); + cy.get(topologyPO.switcher).should('have.attr', 'aria-label', 'List view'); +}); + +Then( + 'user will see the the workload {string} selected with sidebar open', + (workloadName: string) => { + // topologySidePane.verify(); + // topologySidePane.verifyTitle(workloadName); + cy.log(workloadName, 'sidebar is open'); // to avoid lint issues + }, +); + +Given('user has installed Service Binding operator', () => { + verifyAndInstallOperator(operators.ServiceBinding); +}); + +Given('user is at developer perspective', () => { + perspective.switchTo(switchPerspective.Developer); + guidedTour.close(); + nav.sidenav.switcher.shouldHaveText(switchPerspective.Developer); +}); + +When('user right clicks on workload {string}', (appName: string) => { + topologyPage.rightClickOnNode(appName); +}); + +When('user clicks on {string} option from context menu', (actionItem: string) => { + app.waitForLoad(); + topologyActions.selectAction(actionItem); +}); + +Then('user will see {string} modal', (modalName: string) => { + app.waitForLoad(); + cy.get('[aria-label="Modal"]') + .should('be.visible') + .should('contain', modalName); +}); + +Then('user will see alert {string}', (alertName: string) => { + app.waitForDocumentLoad(); + cy.get('[aria-label="Default Alert"]') + .should('be.visible') + .should('contain', alertName); + modal.cancel(); +}); + +Given('user has installed Crunchy Postgres for Kubernetes operator', () => { + verifyAndInstallOperator(operators.CrunchyPostgresforKubernetes); +}); + +Given('user navigates to Topology Page', () => { + perspective.switchTo(switchPerspective.Developer); + projectNameSpace.selectOrCreateProject('aut-topology-delete-workload'); + navigateTo(devNavigationMenu.Topology); + app.waitForLoad(); + topologyPage.verifyTopologyPage(); +}); + +Then('user is able to see Start building your application, Add page links', () => { + cy.get(topologyPO.emptyView.startBuildingYourApplicationLink).should('be.visible'); + cy.get(topologyPO.emptyView.addPageLink).should('be.visible'); +}); + +Then('Display options dropdown, Filter by resource and Find by name fields are disabled', () => { + cy.contains('Display options').should('be.disabled'); + cy.get(topologyPO.filterByResourceDropDown).should('be.disabled'); + cy.get(topologyPO.search).should('be.disabled'); +}); + +Then('switch view is disabled', () => { + cy.get(topologyPO.switcher).should('have.attr', 'aria-disabled', 'true'); +}); + +When('user right clicks on the node {string} to open context menu', (nodeName: string) => { + topologyPage.rightClickOnNode(nodeName); +}); + +Then( + 'user is able to see context menu options like Edit Application Grouping, Edit Pod Count, Pause Rollouts, Add Health Checks, Add Horizontal Pod Autoscaler, Add Storage, Edit Update Strategy, Edit Labels, Edit Annotations, Edit Deployment, Delete Deployment', + () => { + cy.get(chartAreaPO.editApplicationGrouping).should('be.visible'); + cy.get(chartAreaPO.editPodCount).should('be.visible'); + cy.get(chartAreaPO.pauseRollouts).should('be.visible'); + cy.get(chartAreaPO.addHealthChecks).should('be.visible'); + cy.get(chartAreaPO.addHorizontalPod).should('be.visible'); + cy.get(chartAreaPO.addStorage).should('be.visible'); + cy.get(chartAreaPO.editUpdateStrategy).should('be.visible'); + cy.get(chartAreaPO.editLabels).should('be.visible'); + cy.get(chartAreaPO.editAnnotations).should('be.visible'); + cy.get(chartAreaPO.editDeployment).should('be.visible'); + cy.get(chartAreaPO.deleteDeployment).should('be.visible'); + }, +); + +When('user right clicks on the empty chart area', () => { + cy.get(chartAreaPO.topologyArea).rightclick(10, 10); +}); + +When('user hovers on Add to Project', () => { + cy.get(chartAreaPO.addToProject).trigger('mouseover'); +}); + +Then( + 'user is able to see options like Samples, Import from Git, Container Image, From Dockerfile, From Devfile, From Catalog, Database, Operator Backed, Helm Charts, Event Source, Channel', + () => { + cy.get(chartAreaPO.samples).should('be.visible'); + cy.get(chartAreaPO.importFromGit).should('be.visible'); + cy.get(chartAreaPO.containerImage).should('be.visible'); + cy.get(chartAreaPO.catalog).should('be.visible'); + cy.get(chartAreaPO.database).should('be.visible'); + cy.get(chartAreaPO.operatorBacked).should('be.visible'); + cy.get(chartAreaPO.helmCharts).should('be.visible'); + cy.get(chartAreaPO.eventsource).should('be.visible'); + cy.get(chartAreaPO.channel).should('be.visible'); + }, +); + +When('user clicks on Samples', () => { + cy.get(chartAreaPO.samples).click(); +}); + +When('user selects go sample', () => { + cy.get(chartAreaPO.filterItem).type('Go'); + cy.get(chartAreaPO.sampleGo).click(); +}); + +When('user hovers on Add to Project and clicks on {string}', (optionName: string) => { + cy.get(chartAreaPO.topologyArea).rightclick(10, 10); + cy.get(chartAreaPO.addToProject).trigger('mouseover'); + addToProjectOptions(optionName); +}); + +When('user fills the {string} form and clicks Create', (optionName: string) => { + createWorkloadUsingOptions(optionName); +}); + +When( + 'user fills the {string} form with yaml at {string} and clicks Create', + (optionName: string, yamlLocation: string) => { + createWorkloadUsingOptions(optionName, yamlLocation); + }, +); + +When('user selects Python Builder Image and clicks Create Application', () => { + cy.get(chartAreaPO.filterItem).type('python'); + cy.get(chartAreaPO.pythonBuilderImage).click(); + cy.get(chartAreaPO.overlayCreate).click({ force: true }); +}); + +When('user selects Postgres Database and clicks on Instantiate Template', () => { + cy.get(chartAreaPO.filterItem).type('postgresql'); + cy.get(chartAreaPO.postgresqlTemplate).click(); + cy.get(chartAreaPO.overlayCreate).click({ force: true }); +}); + +When('user selects Redis and clicks on Create', () => { + cy.get(chartAreaPO.filterItem).type('redis'); + cy.get(chartAreaPO.operatorBackedRedis).click(); + cy.get(chartAreaPO.overlayCreate).click({ force: true }); +}); + +When('user selects Nodejs and clicks on Install Helm Charts', () => { + cy.get(chartAreaPO.filterItem).type('nodejs'); + cy.get(chartAreaPO.helmNodejs) + .eq(0) + .click(); + cy.get(chartAreaPO.overlayCreate).click({ force: true }); +}); + +When('user selects Api Server Source and clicks on Create Event Source', () => { + cy.get(chartAreaPO.filterItem).type('ApiServerSource'); + cy.get(chartAreaPO.apiEventSource).click(); + cy.get(chartAreaPO.overlayCreate).click({ force: true }); +}); + +When('user clicks on Create button', () => { + cy.get(chartAreaPO.contentScrollable) + .contains('Create') + .click(); +}); + +Then( + 'user is able to see different applications created from Samples, Import from Git, Container Image, From Catalog, Database, Operator Backed, Helm Charts, Event Source, Channel', + () => { + verifyMultipleWorkloadInTopologyPage([ + 'golang-sample', + 'nodejs-ex-git', + 'hello-openshift', + 'python-app', + 'postgres', + 'redis-standalone', + 'helm-nodejs', + 'api-server-source', + 'channel', + ]); + }, +); + +Given('user has created namespace {string}', (projectName: string) => { + Cypress.env('NAMESPACE', projectName); + projectNameSpace.selectOrCreateProject(`${projectName}`); +}); + +Given('user has installed Redis Operator', () => { + verifyAndInstallOperator(operators.RedisOperator); +}); + +Given('user has installed Red Hat OpenShift distributed tracing platform', () => { + verifyAndInstallOperator(operators.Jaeger); +}); + +Given( + 'user has created a operator backed service of {string} operator named {string}', + (operatorName, name: string) => { + navigateTo(devNavigationMenu.Add); + createOperatorBacked(operatorName, name); + }, +); + +Given( + 'user has created a operator backed service {string} from yaml {string}', + (name: string, location: string) => { + createWorkloadUsingOptions('Operator Backed', location); + topologyHelper.verifyWorkloadInTopologyPage(name); + }, +); + +Given( + 'user has created service binding connnector {string} between {string} and {string}', + (bindingName, node1, node2: string) => { + createServiceBindingConnect(bindingName, node1, node2); + }, +); + +When('user clicks on service binding connector', () => { + cy.byLegacyTestID('edge-handler') + .should('be.visible') + .click(); +}); + +When('user clicks on the service binding name {string} at the sidebar', (bindingName: string) => { + cy.byLegacyTestID(`${bindingName}`) + .should('be.visible') + .click(); +}); + +Then('user will see {string} Status on Service binding details page', (status: string) => { + cy.byTestID('resource-status').should('have.text', status); + cy.exec(`oc delete namespace ${Cypress.env('NAMESPACE')}`, { failOnNonZeroExit: false }); +}); + +Then('user will see service binding connection', () => { + cy.byLegacyTestID('edge-handler').should('be.visible'); +}); + +When('user clicks on import YAML button from topology page', () => { + app.waitForLoad(); + topologyPage.verifyTopologyPage(); + cy.get('[data-test="import-yaml"]').click(); + cy.get('.yaml-editor').should('be.visible'); +}); + +When('user enters yaml content from yaml file {string} in the editor', (yamlFile: string) => { + const yamlContent = `support/${yamlFile}`; + yamlEditor.isLoaded(); + yamlEditor.clearYAMLEditor(); + yamlEditor.setEditorContent(yamlContent); +}); + +When('user clicks on Create button in import YAML', () => { + yamlEditor.clickSave(); +}); + +When('user sees {string} Title on Service binding details page', (title: string) => { + app.waitForLoad(); + cy.get('[data-test-id="resource-title"]').should('have.text', title); +}); + +Then( + 'user will see {string} in Label Selector section on Service binding details page', + (label: string) => { + cy.byTestID('label-list').should('have.text', label); + }, +); + +Then( + 'user will see {string} in Label Selector section on Service binding connnector topology sidebar', + (label: string) => { + topologySidePane.selectTab('Details'); + cy.byTestID('label-list').should('have.text', label); + }, +); diff --git a/integration-tests/support/step-definitions/topology/create-workloads.ts b/integration-tests/support/step-definitions/topology/create-workloads.ts new file mode 100644 index 0000000..cb730b5 --- /dev/null +++ b/integration-tests/support/step-definitions/topology/create-workloads.ts @@ -0,0 +1,30 @@ +import { When, Then } from 'cypress-cucumber-preprocessor/steps'; +import { switchPerspective } from '@console/dev-console/integration-tests/support/constants'; +import { perspective, topologyPage } from '@console/dev-console/integration-tests/support/pages'; + +When('user applies cronjob YAML', () => { + cy.exec(`oc apply -f testData/yamls/create-cronjob.yaml`); +}); + +Then('user will see cron job with name {string} on topology page', (name: string) => { + perspective.switchTo(switchPerspective.Developer); + topologyPage.verifyWorkloadInTopologyPage(`${name}`); +}); + +When('user applies job YAML', () => { + cy.exec(`oc apply -f testData/yamls/create-job.yaml`); +}); + +Then('user will see job with name {string} on topology page', (name: string) => { + perspective.switchTo(switchPerspective.Developer); + topologyPage.verifyWorkloadInTopologyPage(`${name}`); +}); + +When('user applies pod YAML', () => { + cy.exec(`oc apply -f testData/yamls/create-pod.yaml`); +}); + +Then('user will see pod with name {string} on topology page', (name: string) => { + perspective.switchTo(switchPerspective.Developer); + topologyPage.verifyWorkloadInTopologyPage(`${name}`); +}); diff --git a/integration-tests/support/step-definitions/topology/delete-workload.ts b/integration-tests/support/step-definitions/topology/delete-workload.ts new file mode 100644 index 0000000..cfa1df2 --- /dev/null +++ b/integration-tests/support/step-definitions/topology/delete-workload.ts @@ -0,0 +1,18 @@ +import { When } from 'cypress-cucumber-preprocessor/steps'; +import { app } from '@console/dev-console/integration-tests/support/pages'; +import { topologyActions } from '@console/topology/integration-tests/support/pages/topology/topology-actions-page'; +import { topologySidePane } from '@console/topology/integration-tests/support/pages/topology/topology-side-pane-page'; +import { topologyPO } from '../../page-objects/topology-po'; + +When('user clicks on Action menu', () => { + topologySidePane.clickActionsDropDown(); +}); + +When('user clicks {string} from action menu', (actionItem: string) => { + app.waitForLoad(); + topologyActions.selectAction(actionItem); +}); + +When('user clicks on Delete button from modal', () => { + cy.get(topologyPO.graph.deleteWorkload).click(); +}); diff --git a/integration-tests/support/step-definitions/topology/edit-workload.ts b/integration-tests/support/step-definitions/topology/edit-workload.ts new file mode 100644 index 0000000..7bcf35e --- /dev/null +++ b/integration-tests/support/step-definitions/topology/edit-workload.ts @@ -0,0 +1,264 @@ +import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps'; +import { devNavigationMenu } from '@console/dev-console/integration-tests/support/constants'; +import { formPO } from '@console/dev-console/integration-tests/support/pageObjects'; +import { app, navigateTo } from '@console/dev-console/integration-tests/support/pages'; +import { gitPage } from '@console/dev-console/integration-tests/support/pages/add-flow'; +import { pipelineBuilderPO } from '@console/pipelines-plugin/integration-tests/support/page-objects'; +import { addSecret } from '@console/topology/integration-tests/support/pages/functions/add-secret'; +import { topologyHelper } from '@console/topology/integration-tests/support/pages/topology/topology-helper-page'; +import { topologyPO } from '../../page-objects/topology-po'; +import { topologyPage, topologySidePane } from '../../pages/topology'; +import { editDeployment } from '../../pages/topology/topology-edit-deployment'; + +When('user edits application groupings to {string}', (newAppName: string) => { + gitPage.editAppName(newAppName); +}); + +When('user saves the new changes', () => { + cy.get(topologyPO.createSecret.formInputs.saveSecret).click(); +}); + +Then('user can see application groupings updated to {string}', (newAppName: string) => { + topologyHelper.verifyWorkloadInTopologyPage(newAppName); +}); + +When('user clicks on Show advanced image options', () => { + cy.get(pipelineBuilderPO.formView.switchToFormView).click(); + if (cy.get(topologyPO.createSecret.advancedOptions).contains('Show advanced image options')) { + cy.get(topologyPO.createSecret.advancedOptions).click(); + } else { + cy.log('You have already opened advanced options'); + } +}); + +When('user clicks on Create new secret', () => { + cy.get(topologyPO.createSecret.createSecretButton) + .contains('Create new Secret') + .click(); +}); + +When('user creates a new secret {string}', (secretName: string) => { + addSecret(secretName); +}); + +When('user clicks on {string} from context action menu', (actionItem: string) => { + cy.byTestActionID(actionItem) + .should('be.visible') + .click(); +}); + +Then('user will see {string} in secret name dropdown under Pull secret', (secretName: string) => { + cy.reload(); + if (cy.get(topologyPO.createSecret.advancedOptions).contains('Show advanced image options')) { + cy.get(topologyPO.createSecret.advancedOptions).click(); + } else { + cy.log('You have already opened advanced options'); + } + cy.get(topologyPO.createSecret.secretDropDown).click(); + cy.get(topologyPO.deploymentStrategy.dropdownSecret).type(secretName); + cy.get(topologyPO.createSecret.secretDropDownItem, { timeout: 50000 }).should( + 'contain', + secretName, + ); + navigateTo(devNavigationMenu.Topology); +}); + +When('user enters value of CPU Request as {string}', (requestCPU: string) => { + cy.get(topologyPO.resourceLimits.requestCPU) + .clear() + .type(requestCPU); +}); + +When('user enters value of CPU Limit as {string}', (limitCPU: string) => { + cy.get(topologyPO.resourceLimits.limitCPU) + .clear() + .type(limitCPU); +}); + +When('user enters value of Memory Request as {string}', (requestMemory: string) => { + cy.get(topologyPO.resourceLimits.requestMemory) + .clear() + .type(requestMemory); +}); + +When('user enters value of Memory Limit as {string}', (limitMemory: string) => { + cy.get(topologyPO.resourceLimits.limitMemory) + .clear() + .type(limitMemory); +}); + +When('user clicks on Save', () => { + cy.get(topologyPO.createSecret.formInputs.saveSecret).click(); +}); + +Then('user will be redirected to Topology with the updated resource limits', () => { + cy.get(topologyPO.graph.emptyGraph).should('be.visible'); +}); + +When('user selects {string} Strategy type under Deployment Strategy', (strategyType: string) => { + cy.wait(50000); + cy.get(formPO.cancel).click(); + editDeployment.selectDeploymentStrategyType(strategyType); +}); + +When( + 'user enters value of Maximum number of unavailable Pods and Maximum number of surge Pods as {string}', + (podPercentage: string) => { + cy.get(topologyPO.deploymentStrategy.maxUnavailablePods) + .clear() + .type(podPercentage); + cy.get(topologyPO.deploymentStrategy.maxSurgePods) + .clear() + .type(podPercentage); + }, +); + +When( + 'user selects value of project, image stream and tag section under images as {string}, {string} and {string} respectively', + (projectName: string, imageStream: string, tag: string) => { + editDeployment.selectProjectName(projectName); + editDeployment.selectImageStream(imageStream); + editDeployment.selectImageStreamTag(tag); + }, +); + +When('user enters NAME as {string} and VALUE as {string}', (envName: string, envValue: string) => { + cy.get(topologyPO.deploymentStrategy.envName) + .clear() + .type(envName); + cy.get(topologyPO.deploymentStrategy.envValue) + .clear() + .type(envValue); +}); + +When('user selects secret {string} from Pull secret dropdown', (secretName: string) => { + cy.get(topologyPO.deploymentStrategy.selectSecret).click(); + cy.get(topologyPO.deploymentStrategy.dropdownSecret).type(secretName); + const CSSSelector = `[id="${secretName}-link"]`; + cy.get(CSSSelector, { timeout: 50000 }).click(); +}); + +When('user selects the Pause rollouts check box under advanced options section', () => { + cy.get(topologyPO.deploymentStrategy.advancedOptions) + .eq(0) + .click(); + cy.get(topologyPO.deploymentStrategy.pauseRolloutsCheckbox).click(); +}); + +When('user enters Replicas as {string} under Scaling section', (replicaCount: string) => { + cy.get(topologyPO.deploymentStrategy.advancedOptions) + .find('Scaling') + .click(); + cy.get(topologyPO.deploymentStrategy.enterReplica) + .clear() + .type(replicaCount); +}); + +When('user will be redirected to Topology with the updated deployment', () => { + cy.get(topologyPO.graph.emptyGraph).should('be.visible'); +}); + +When('user saves the changes', () => { + cy.get(topologyPO.deploymentStrategy.saveEdit).click(); +}); + +When('user enters Timeout value as {string}', (timeoutValue: string) => { + cy.get(topologyPO.deploymentStrategy.timeout) + .clear() + .type(timeoutValue); +}); + +When('user clicks on Show additional parameters and lifcycle hooks', () => { + cy.get(topologyPO.createSecret.advancedOptions) + .eq(0) + .click(); +}); + +When( + 'user adds Pre Cycle Hook for workload {string} with failure policy {string}', + (workloadName: string, failurePolicy: string) => { + cy.contains('Add pre lifecycle hook').click(); + editDeployment.addPreCycleHook(workloadName, failurePolicy); + }, +); + +When( + 'user adds Mid Cycle Hook for workload {string} with failure policy {string}', + (workloadName: string, failurePolicy: string) => { + cy.contains('Add mid lifecycle hook').click(); + editDeployment.addMidCycleHook(workloadName, failurePolicy); + }, +); + +When( + 'user adds Post Cycle Hook for workload {string} with failure policy {string}', + (workloadName: string, failurePolicy: string) => { + cy.contains('Add post lifecycle hook').click(); + editDeployment.addPostCycleHook(workloadName, failurePolicy); + }, +); + +When('user unchecks Deploy image from an image stream tag checkbox', () => { + cy.get(topologyPO.deploymentStrategy.deployImageCheckbox).click(); +}); + +When('user enters value of Image name as {string}', (imageLink: string) => { + cy.get(topologyPO.deploymentStrategy.imageName) + .clear() + .type(imageLink); +}); + +When('user enters key as {string}', (annotationKey: string) => { + cy.get(topologyPO.graph.addNewAnnotations).click(); + cy.get(topologyPO.deploymentStrategy.envName) + .last() + .clear() + .type(annotationKey); +}); + +When('user enters value as {string} to which it will be connected', (annotationValue: string) => { + cy.get(topologyPO.deploymentStrategy.envValue) + .last() + .clear() + .type(annotationValue); + cy.get(topologyPO.graph.deleteWorkload).click(); +}); + +Then('user can see that two workloads are connected with arrow', () => { + navigateTo(devNavigationMenu.Add); + navigateTo(devNavigationMenu.Topology); + cy.get(topologyPO.graph.connector, { timeout: 10000 }).should('be.visible'); +}); + +Then( + 'user right clicks on the knative workload {string} to open the Context Menu', + (nodeName: string) => { + topologyPage.getKnativeNode(nodeName).trigger('contextmenu', { force: true }); + }, +); +export const checkBuildComplete = (tries: number = 4) => { + if (tries < 1) { + return; + } + // eslint-disable-next-line promise/catch-or-return + cy.get('body').then(($body) => { + if ($body.find('.build-overview__status svg title').length === 0) { + cy.reload(); + app.waitForDocumentLoad(); + cy.wait(30000); + checkBuildComplete(tries - 1); + } else { + cy.log('COMPLETE'); + } + }); +}; + +Given( + 'user clicks on knative workload {string} to verify that build is completed', + (nodeName: string) => { + topologyPage.knativeNode(nodeName).click({ force: true }); + topologySidePane.selectTab('Resources'); + checkBuildComplete(); + cy.byLegacyTestID('sidebar-close-button').click(); + }, +); diff --git a/integration-tests/support/step-definitions/topology/horizontal-pod-autoscaler/action-on-hpa.ts b/integration-tests/support/step-definitions/topology/horizontal-pod-autoscaler/action-on-hpa.ts new file mode 100644 index 0000000..9c407d4 --- /dev/null +++ b/integration-tests/support/step-definitions/topology/horizontal-pod-autoscaler/action-on-hpa.ts @@ -0,0 +1,256 @@ +import { When, Then, Given } from 'cypress-cucumber-preprocessor/steps'; +import { + devNavigationMenu, + switchPerspective, +} from '@console/dev-console/integration-tests/support/constants'; +import { + app, + createGitWorkloadWithResourceLimit, + navigateTo, + perspective, + topologyPage, + topologySidePane, +} from '@console/dev-console/integration-tests/support/pages'; +import { hpaPO } from '../../../page-objects/hpa-po'; +import { checkPodsCount } from '../../../pages/functions/add-secret'; + +Given( + 'user has created a deployment workload {string} with CPU resource limit {string} and Memory resource limit {string}', + (workloadName: string, limitCPU: string, limitMemory: string) => { + navigateTo(devNavigationMenu.Add); + createGitWorkloadWithResourceLimit( + 'https://github.com/sclorg/nodejs-ex.git', + workloadName, + 'Deployment', + 'nodejs-ex-git-app', + limitCPU, + limitMemory, + ); + }, +); + +Given('user is at administrator perspective', () => { + perspective.switchTo(switchPerspective.Administrator); +}); + +Given('user is at developer perspective', () => { + perspective.switchTo(switchPerspective.Developer); + cy.testA11y('Developer perspective with guider tour modal'); +}); + +Given('user is at HorizontalPodAutoscaler page under workloads section', () => { + cy.get(hpaPO.navWorkloads).click(); + cy.get(hpaPO.nav) + .contains('HorizontalPodAutoscalers') + .click(); +}); + +Given('user has created HorizontalPodAutoscaler', () => { + cy.get(hpaPO.createItem) + .contains('Create HorizontalPodAutoscaler') + .click(); + cy.get(hpaPO.saveChanges).click(); +}); + +Given( + 'user can see a deployment workload {string} created with HPA assigned to it', + (workloadName: string) => { + topologyPage.componentNode(workloadName).should('be.visible'); + }, +); + +Given('user has a deployment workload {string} with HPA assigned to it', (workloadName) => { + navigateTo(devNavigationMenu.Add); + createGitWorkloadWithResourceLimit( + 'https://github.com/sclorg/nodejs-ex.git', + workloadName, + 'Deployment', + 'nodejs-ex-git-app', + ); + cy.get(hpaPO.sidebarClose).click(); + topologyPage.componentNode(workloadName).click({ force: true }); + cy.get(hpaPO.actionMenu).click(); + cy.byTestActionID('Add HorizontalPodAutoscaler').click(); + cy.get(hpaPO.cpu) + .clear() + .type('50'); + cy.get(hpaPO.memory) + .clear() + .type('50'); + cy.get(hpaPO.submitBtn).click(); + cy.get(hpaPO.switchToggler).should('be.visible'); +}); + +When('user clicks the HPA associated with the workload', () => { + cy.get(hpaPO.resourceTitle).should('include.text', 'example'); + cy.get(hpaPO.hpaHeading).should('be.visible'); + cy.get(hpaPO.nav) + .contains('HorizontalPodAutoscalers') + .click(); + cy.get(hpaPO.itemFilter).type('example'); + cy.get(hpaPO.resourceItem) + .contains('example') + .click(); +}); + +When('user selects {string} option from Actions menu in HPA details page', (option: string) => { + cy.get(hpaPO.resourceTitle).should('include.text', 'example'); + cy.get(hpaPO.actionMenu).click(); + cy.byTestActionID(option).click(); +}); + +When('user sees Delete Horizontal Pod Autoscaler modal opens', () => { + cy.get(hpaPO.modalTitle) + .should('be.visible') + .contains('Delete HorizontalPodAutoscaler?'); +}); + +When('user clicks Delete', () => { + cy.get(hpaPO.deleteOption).click(); +}); + +Then('user can see the intended HPA is deleted', () => { + cy.get(hpaPO.exampleHPA).should('not.exist'); + cy.get(hpaPO.emptyMessage).contains('No HorizontalPodAutoscalers found'); +}); + +Given('user is at Topology page', () => { + navigateTo(devNavigationMenu.Topology); +}); + +When('user clicks on workload {string}', (workloadName: string) => { + topologyPage.waitForLoad(); + topologyPage.componentNode(workloadName).click({ force: true }); +}); + +When('user selects {string} option from Actions menu', (option: string) => { + cy.get(hpaPO.actionMenu).click(); + cy.byTestActionID(option).click(); +}); + +When('user enters Name as {string} in Horizontal Pod Autoscaler page', (hpaName: string) => { + cy.get(hpaPO.nameHPA) + .clear() + .type(hpaName); +}); + +When('user sets Minimum Pods value to {string}', (minPod: string) => { + cy.get(hpaPO.minhpaPod) + .clear() + .type(minPod); +}); + +When('user sets Maximum Pods value to {string}', (maxPod: string) => { + cy.get(hpaPO.maxhpaPod) + .clear() + .type(maxPod); +}); + +When('user enters CPU Utilization as {string}', (cpuUtilisation: string) => { + cy.get(hpaPO.cpu) + .clear() + .type(cpuUtilisation); +}); + +When('user enters Memory Utilization as {string}', (memoryUtilization: string) => { + cy.get(hpaPO.memory) + .clear() + .type(memoryUtilization); +}); + +When('user clicks on Save button', () => { + cy.get(hpaPO.submitBtn).click(); +}); + +Then('user can see sidebar opens with Resources tab selected by default', () => { + topologySidePane.verifySelectedTab('Resources'); +}); + +Then('user can see two pods under pod section', () => { + checkPodsCount(); + cy.get(hpaPO.nodeList, { timeout: 120000 }).should('have.length.within', 2, 5); +}); + +Then('user can see HorizontalPodAutoscalers section', () => { + cy.get(hpaPO.sectionHeading).contains('HorizontalPodAutoscalers'); +}); + +Then( + 'user can see the {string} with HPA tag associated present under HPA section', + (hpaName: string) => { + cy.byLegacyTestID(hpaName).should('be.visible'); + }, +); + +When('user sees values are prefilled', () => { + cy.get(hpaPO.hpaFormName).should('not.be.empty'); + cy.get(hpaPO.maxhpaPod).should('not.be.empty'); + cy.get(hpaPO.minhpaPod).should('not.be.empty'); + cy.get(hpaPO.cpu).should('not.be.empty'); + cy.get(hpaPO.memory).should('not.be.empty'); +}); + +When('user checks the name value but cannot edit it', () => { + cy.get(hpaPO.hpaFormName).should('have.value', 'example'); + cy.get(hpaPO.hpaFormName).should('be.disabled'); +}); + +When('user checks and edit the value to {string} for maximum pods', (maxPod: string) => { + cy.get(hpaPO.maxhpaPod) + .clear() + .type(maxPod); +}); + +When('user checks and edit the value to {string} for minimum pods', (minPod: string) => { + cy.get(hpaPO.minhpaPod) + .clear() + .type(minPod); +}); + +When('user checks and edit the cpu value to {string}', (cpuUtilisation: string) => { + cy.get(hpaPO.cpu) + .clear() + .type(cpuUtilisation); +}); + +When('user checks and edit the memory value to {string}', (memoryUtilization: string) => { + cy.get(hpaPO.memory) + .clear() + .type(memoryUtilization); +}); + +Then('user goes to the details tab', () => { + topologySidePane.selectTab('Details'); +}); + +Then('user can see the changed pods number', () => { + cy.reload(); + app.waitForDocumentLoad(); + topologySidePane.selectTab('Details'); + cy.get(hpaPO.podRing, { timeout: 80000 }).should('include.text', '3 Pods'); + cy.get(hpaPO.sidebarClose).click(); +}); + +When('user right clicks on workload {string}', (appName: string) => { + topologyPage.rightClickOnNode(appName); +}); + +When('user selects {string} option from context menu', (actionItem: string) => { + cy.byTestActionID(actionItem).click(); +}); + +When('user clicks Remove on Remove HorizontalPodAutoscaler? modal', () => { + cy.get(hpaPO.deleteOption).click(); +}); + +When('user selects Resources tab on sidebar', () => { + topologySidePane.selectTab('Resources'); +}); + +Then('user can not see HorizontalPodAutoscalers section', () => { + cy.reload(); + app.waitForDocumentLoad(); + cy.get(hpaPO.sectionHeading) + .contains('HorizontalPodAutoscalers') + .should('not.exist'); +}); diff --git a/integration-tests/support/step-definitions/topology/list-view.ts b/integration-tests/support/step-definitions/topology/list-view.ts new file mode 100644 index 0000000..e823c76 --- /dev/null +++ b/integration-tests/support/step-definitions/topology/list-view.ts @@ -0,0 +1,22 @@ +import { When, Then } from 'cypress-cucumber-preprocessor/steps'; +import { devNavigationMenu } from '@console/dev-console/integration-tests/support/constants/global'; +import { navigateTo } from '@console/dev-console/integration-tests/support/pages/app'; +import { topologyPO } from '../../page-objects/topology-po'; +import { topologyPage } from '../../pages/topology'; + +When('user clicks on List view button', () => { + navigateTo(devNavigationMenu.Topology); + if (cy.get(topologyPO.graph.emptyGraph)) { + cy.get(topologyPO.switcher).click(); + } else { + cy.log('You are already on List View'); + } +}); + +Then('user will see workloads are segregated by applications groupings', () => { + cy.get(topologyPO.graph.applicationGroupingTitle).should('be.visible'); +}); + +Then('user will see resource quota alert in {string} list node', (nodeName: string) => { + topologyPage.verifyListNodeAlert(nodeName); +}); diff --git a/integration-tests/support/step-definitions/topology/pipelines-on-topology.ts b/integration-tests/support/step-definitions/topology/pipelines-on-topology.ts new file mode 100644 index 0000000..107d9c9 --- /dev/null +++ b/integration-tests/support/step-definitions/topology/pipelines-on-topology.ts @@ -0,0 +1,176 @@ +import { When, Then, Given } from 'cypress-cucumber-preprocessor/steps'; +import { pageTitle } from '@console/dev-console/integration-tests/support/constants'; +import { + devNavigationMenu, + switchPerspective, +} from '@console/dev-console/integration-tests/support/constants/global'; +import { + createGitWorkload, + createGitWorkloadWithBuilderImage, +} from '@console/dev-console/integration-tests/support/pages'; +import { navigateTo, perspective } from '@console/dev-console/integration-tests/support/pages/app'; +import { topologyPO } from '../../page-objects/topology-po'; +import { topologyPage, topologySidePane } from '../../pages/topology'; + +Given( + 'user has created workload {string} with resource type {string} with pipeline', + (componentName: string, resourceName: string) => { + navigateTo(devNavigationMenu.Add); + createGitWorkload( + 'https://github.com/sclorg/nodejs-ex.git', + componentName, + resourceName, + 'nodejs-ex-git-app', + true, + ); + }, +); + +Given( + 'user has created workload {string} with resource type {string} without pipeline', + (componentName: string, resourceName: string) => { + navigateTo(devNavigationMenu.Add); + createGitWorkload( + 'https://github.com/sclorg/nodejs-ex.git', + componentName, + resourceName, + 'nodejs-ex-git-app', + false, + ); + }, +); + +Given( + 'user has created workload {string} with resource type {string} and builder image {string} with pipeline', + (componentName: string, resourceName: string, builderImage: string) => { + navigateTo(devNavigationMenu.Add); + createGitWorkloadWithBuilderImage( + 'https://github.com/sclorg/nodejs-ex.git', + componentName, + resourceName, + builderImage, + 'nodejs-ex-git-app', + true, + ); + }, +); + +When('user navigates to Topology page', () => { + navigateTo(devNavigationMenu.Topology); +}); + +When('user clicks on workload {string} to open sidebar', (workloadName: string) => { + topologyPage.componentNode(workloadName).click({ force: true }); + topologyPage.waitForLoad(); +}); + +Then('user can see {string} section in Resources tab', (heading: string) => { + topologySidePane.selectTab('Resources'); + topologySidePane.verifyTab('Resources'); + topologySidePane.verifySection(heading); +}); + +When('user goes to the pipelines page', () => { + navigateTo(devNavigationMenu.Pipelines); +}); + +Then('user can see the {string} pipeline is succeeded', (workloadName: string) => { + cy.byLegacyTestID('item-filter') + .clear() + .type(workloadName); + cy.byTestID('status-text', { timeout: 180000 }).should('include.text', 'Succeeded'); +}); + +When('user goes to the Administrator perspective', () => { + perspective.switchTo(switchPerspective.Administrator); +}); + +When('user clicks on the Persistent Volume Claims in Storage tab', () => { + cy.get(topologyPO.pipelines.storageNav).click(); + cy.get(topologyPO.pipelines.pvcOption) + .eq(0) + .click(); +}); + +When('user can see workspace created for the resource', () => { + cy.get(topologyPO.pipelines.pvc) + .invoke('attr', 'aria-rowcount') + .should('have.length.greaterThan', 0); +}); + +When('user clicks on Start on the {string} pipeline', (workloadName: string) => { + cy.byLegacyTestID('item-filter') + .clear() + .type(workloadName); + cy.wait(2000); + cy.byLegacyTestID('kebab-button').click(); + cy.get(topologyPO.pipelines.startAction).click(); +}); + +When('user can see "PVC" in workspace with name of PVC', () => { + cy.byLegacyTestID('dropdown-button').should('include.text', 'PersistentVolumeClaim'); + cy.get(topologyPO.pipelines.pvcIcon).should('include.text', 'PVC'); + cy.byLegacyTestID('modal-cancel-action').click(); +}); + +When('user clicks on Add Trigger on the {string} pipeline', (workloadName: string) => { + cy.byLegacyTestID('item-filter') + .clear() + .type(workloadName); + cy.wait(2000); + cy.byLegacyTestID('kebab-button').click(); + cy.get(topologyPO.pipelines.addTriggerAction).click(); +}); + +When('user clicks on Edit {string} from action menu', (workloadName: string) => { + navigateTo(devNavigationMenu.Topology); + topologyPage.waitForLoad(); + topologyPage.componentNode(workloadName).click({ force: true }); + cy.byLegacyTestID('actions-menu-button').click(); + cy.byTestActionID(`Edit ${workloadName}`).click(); +}); + +When('user can see Pipeline checkbox is disabled', () => { + cy.get(topologyPO.pipelines.pipelineCheckbox).should('be.disabled'); +}); + +When('user can not see Build configuration option in Advanced Options', () => { + cy.get(topologyPO.pipelines.editWorkloadPage).should('not.include.text', 'Build Configuration'); +}); + +When('user can see Pipeline section is present', () => { + cy.get(topologyPO.pipelines.pipelineSection) + .scrollIntoView() + .should('be.visible'); +}); + +When('user can see Pipeline checkbox is present in enabled state', () => { + cy.get(topologyPO.pipelines.pipelineCheckbox).should('be.enabled'); +}); + +When('user checks the Pipeline checkbox to disable build configuration in Advanced Options', () => { + cy.get(topologyPO.pipelines.pipelineCheckbox).check(); + cy.get(topologyPO.pipelines.editWorkloadPage).should('not.include.text', 'Build Configuration'); +}); + +Then('user can see PipelineRuns section is present', () => { + topologySidePane.verifyTab('Resources'); + topologySidePane.verifySection(pageTitle.PipelineRuns); +}); + +Then('user can see Build section is present', () => { + topologySidePane.verifyTab('Resources'); + topologySidePane.verifySection(pageTitle.Builds); +}); + +Then( + 'user edit the workload {string} with {string}', + (workloadName: string, builderImage: string) => { + topologyPage.waitForLoad(); + topologyPage.componentNode(workloadName).click({ force: true }); + cy.byLegacyTestID('actions-menu-button').click(); + cy.byTestActionID(`Edit ${workloadName}`).click(); + cy.byTestID(`card ${builderImage}`).click(); + cy.byTestID('submit-button').click(); + }, +); diff --git a/integration-tests/support/step-definitions/topology/quick-search-topology.ts b/integration-tests/support/step-definitions/topology/quick-search-topology.ts new file mode 100644 index 0000000..5e68ca8 --- /dev/null +++ b/integration-tests/support/step-definitions/topology/quick-search-topology.ts @@ -0,0 +1,211 @@ +import { Given, Then, When } from 'cypress-cucumber-preprocessor/steps'; +import { devNavigationMenu } from '@console/dev-console/integration-tests/support/constants'; +import { + catalogPO, + gitPO, + quickSearchAddPO, +} from '@console/dev-console/integration-tests/support/pageObjects'; +import { + createGitWorkload, + devFilePage, + gitPage, + navigateTo, + topologyHelper, +} from '@console/dev-console/integration-tests/support/pages'; +import { addQuickSearch } from '../../../../../dev-console/integration-tests/support/pages/add-flow/add-quick-search'; +import { topologyPO } from '../../page-objects/topology-po'; +import { topologyPage } from '../../pages/topology/topology-page'; + +Then('user is able to see Start building your application, Add page links', () => { + cy.get(topologyPO.emptyView.startBuildingYourApplicationLink).should('be.visible'); + cy.get(topologyPO.emptyView.addPageLink).should('be.visible'); +}); + +Then('Display options dropdown, Filter by resource and Find by name fields are disabled', () => { + cy.contains('Display options').should('be.disabled'); + cy.get(topologyPO.filterByResourceDropDown).should('be.disabled'); + cy.get(topologyPO.search).should('be.disabled'); +}); + +Then('switch view is disabled', () => { + cy.get(topologyPO.switcher).should('have.attr', 'aria-disabled', 'true'); +}); + +Given('user created a workload and is at topology list view', () => { + navigateTo(devNavigationMenu.Add); + createGitWorkload( + 'https://github.com/sclorg/nodejs-ex.git', + 'nodejs-app', + 'deployment', + 'nodejs-ex-git-app', + false, + ); + navigateTo(devNavigationMenu.Topology); + cy.get(topologyPO.quickSearchPO.listView).click(); +}); + +Given('user is at topology list view', () => { + navigateTo(devNavigationMenu.Topology); + topologyPage.verifyUserIsInListView(); +}); + +Given('user is at topology graph view', () => { + navigateTo(devNavigationMenu.Topology); + topologyPage.verifyUserIsInGraphView(); +}); + +When('user clicks on graph view button', () => { + topologyPage.verifyUserIsInListView(); + cy.get(topologyPO.quickSearchPO.graphView).click(); +}); + +When('user clicks on list view button', () => { + topologyPage.verifyUserIsInGraphView(); + cy.get(topologyPO.quickSearchPO.listView).click(); +}); + +Then('user can see Add to project button', () => { + cy.get(quickSearchAddPO.quickSearchButton).should('be.visible'); +}); + +When('user clicks Add to project button', () => { + cy.get(quickSearchAddPO.quickSearchButton) + .should('be.visible') + .click(); +}); + +Then('user can see Add to project search bar', () => { + cy.get(quickSearchAddPO.quickSearchBar).should('be.visible'); +}); + +When('user enters {string} in Add to project search bar', (node: string) => { + addQuickSearch.enterNodeName(node); +}); + +When('user selects django+PostgreSQL option', () => { + cy.get(topologyPO.quickSearchPO.djangoPostgreSQL).click(); +}); + +When('user clicks on instantiate template', () => { + cy.get(quickSearchAddPO.quickSearchCreateButton) + .should('be.visible') + .click(); +}); + +When('user clicks on create with default values in Instantiate Template form', () => { + cy.get(catalogPO.create) + .scrollIntoView() + .click(); +}); + +Then( + 'user can see {string} and {string} workload in topology graph view', + (workloadName1: string, workloadName2: string) => { + topologyPage.verifyWorkloadInTopologyPage(workloadName1); + topologyPage.verifyWorkloadInTopologyPage(workloadName2); + }, +); + +When('user selects .Net core option', () => { + cy.get(topologyPO.quickSearchPO.NETSample).click(); +}); + +When('user clicks on Create Application', () => { + cy.get(quickSearchAddPO.quickSearchCreateButton) + .should('be.visible') + .click(); +}); + +When('user clicks on create with default values in Create Application form', () => { + cy.get(catalogPO.create) + .scrollIntoView() + .click(); +}); + +Then('user can see {string} workload in topology list view', (workloadName: string) => { + topologyHelper.verifyWorkloadInTopologyPage(workloadName); +}); + +When('user clicks on View all developer catalog items link', () => { + cy.get(quickSearchAddPO.viewInDevCatalog) + .scrollIntoView() + .click(); +}); + +Then('user will see Catalog with {string} text filter', (filterWord: string) => { + cy.get(catalogPO.search).should('be.visible'); + cy.get(catalogPO.search).should('have.value', filterWord); +}); + +Then('user will see No results', () => { + cy.get(topologyPO.quickSearchPO.noResults).should('be.visible'); +}); + +When('user selects Monitor your sample application option', () => { + cy.get(topologyPO.quickSearchPO.monitorApp).click(); +}); + +When('user clicks on Start button', () => { + cy.get(quickSearchAddPO.quickSearchCreateButton) + .should('be.visible') + .click(); +}); + +Then('Monitor your sample application quick start displays in the Topology', () => { + cy.get(topologyPO.quickSearchPO.quickstartDrawer).should('be.visible'); +}); + +When('user clicks on View all quick starts link', () => { + cy.get(topologyPO.quickSearchPO.quickStarts).click(); +}); + +Then('user will be redirected to the search results of the Quick Starts Catalog', () => { + cy.get(topologyPO.quickSearchPO.pageTitle).should('include.text', 'Quick Starts'); + navigateTo(devNavigationMenu.Topology); +}); + +When('user selects Basic NodeJS Devfiles option', () => { + cy.get(topologyPO.quickSearchPO.nodejsDevfiles).click(); +}); + +When('user clicks on Create button in the Import from Devfile page', () => { + devFilePage.verifyValidatedMessage('https://github.com/nodeshift-starters/devfile-sample.git'); + cy.get(gitPO.gitRepoUrl).should( + 'have.value', + 'https://github.com/nodeshift-starters/devfile-sample.git', + ); + gitPage.clickCreate(); +}); + +Then( + 'user is taken to the Topology page with {string} workload created', + (workloadName: string) => { + topologyPage.verifyWorkloadInTopologyPage(workloadName); + }, +); + +When('user selects Basic NodeJS Samples option', () => { + cy.get(topologyPO.quickSearchPO.nodejsSamples).click(); +}); + +When('user clicks on Create Devfile Samples', () => { + cy.get(quickSearchAddPO.quickSearchCreateButton) + .should('be.visible') + .click(); +}); + +When('user enters Name as {string} in Import from Devfile page', (name: string) => { + const gitUrl = 'https://github.com/nodeshift-starters/devfile-sample.git'; + devFilePage.verifyValidatedMessage(gitUrl); + gitPage.enterAppName(`${name}-app`); + gitPage.enterWorkloadName(`${name}`); + cy.get(gitPO.gitRepoUrl).should('have.value', gitUrl); +}); + +When('user clicks on View all Samples link', () => { + cy.get(topologyPO.quickSearchPO.samplePage).click(); +}); + +Then('user is taken to the search results in context of the Samples page', () => { + cy.get(topologyPO.quickSearchPO.resourseTitle).should('include.text', 'Samples'); +}); diff --git a/integration-tests/support/step-definitions/topology/resource-quota-warning.ts b/integration-tests/support/step-definitions/topology/resource-quota-warning.ts new file mode 100644 index 0000000..f164282 --- /dev/null +++ b/integration-tests/support/step-definitions/topology/resource-quota-warning.ts @@ -0,0 +1,90 @@ +import { Given, When, Then, And } from 'cypress-cucumber-preprocessor/steps'; +import { detailsPage } from '@console/cypress-integration-tests/views/details-page'; +import { listPage } from '@console/cypress-integration-tests/views/list-page'; +import { modal } from '@console/cypress-integration-tests/views/modal'; +import * as yamlView from '@console/cypress-integration-tests/views/yaml-editor'; +import { devNavigationMenu } from '@console/dev-console/integration-tests/support/constants'; +import { + navigateTo, + createGitWorkloadIfNotExistsOnTopologyPage, + topologyHelper, + topologyPage, + app, + topologySidePane, +} from '@console/dev-console/integration-tests/support/pages'; +import { topologyPO } from '../../page-objects/topology-po'; + +const deteleResourceQuota = () => { + detailsPage.isLoaded(); + detailsPage.clickPageActionFromDropdown('Delete ResourceQuota'); + modal.shouldBeOpened(); + modal.submit(); + modal.shouldBeClosed(); +}; + +Given('user has created workload with resource type deployment', () => { + createGitWorkloadIfNotExistsOnTopologyPage( + 'https://github.com/sclorg/nodejs-ex.git', + 'ex-node-js', + 'deployment', + 'nodejs-ex-git-app', + ); + topologyHelper.verifyWorkloadInTopologyPage('ex-node-js'); +}); + +Given('user has created two resource quotas using {string} file', (yamlLocation) => { + cy.exec(`oc apply -f ${yamlLocation} -n ${Cypress.env('NAMESPACE')}`); + app.waitForDocumentLoad(); +}); + +When('user navigates to Topology page', () => { + navigateTo(devNavigationMenu.Topology); + topologyPage.verifyTopologyPage(); +}); + +When('user clicks on link to view resource quota details', () => { + cy.byTestID('resource-quota-warning').click(); +}); + +Then('user is redirected to resource quota details page', () => { + cy.get('h2').should('contain.text', 'ResourceQuota details'); + deteleResourceQuota(); +}); + +Then('user is redirected to resource quota list page', () => { + listPage.rows.shouldBeLoaded(); +}); + +When( + 'user creates resource quota {string} by entering {string} file data', + (resourceQuotaName: string, yamlLocation: string) => { + cy.get('[data-test="import-yaml"]').click(); + cy.get('.yaml-editor').should('be.visible'); + cy.readFile(yamlLocation).then((str) => { + const myArray = str.split('---'); + resourceQuotaName === 'resourcequota1' + ? yamlView.setEditorContent(myArray[0]) + : yamlView.setEditorContent(myArray[1]); + }); + cy.get('[data-test="save-changes"]').click(); + cy.get('h2').should('contain.text', 'ResourceQuota details'); + }, +); + +Then('user is able to see resource quota alert', () => { + topologySidePane.verifyResourceQuotaAlert(); +}); + +And('user is able to see yellow border around {string} workload', (workloadName: string) => { + topologyPage.verifyNodeAlert(workloadName); +}); + +And('user continously clicks on zoom-out button until it gets maximum zoomed out', () => { + topologyPage.clickMaxZoomOut(); +}); + +Then('user is able to see yellow background on workload for resource quota alert', () => { + cy.byLegacyTestID('ex-node-js1') + .get(topologyPO.graph.warningBackground) + .should('be.visible'); +}); diff --git a/integration-tests/support/step-definitions/topology/topology-add-options.ts b/integration-tests/support/step-definitions/topology/topology-add-options.ts new file mode 100644 index 0000000..ae705bf --- /dev/null +++ b/integration-tests/support/step-definitions/topology/topology-add-options.ts @@ -0,0 +1,102 @@ +import { Given, Then, When } from 'cypress-cucumber-preprocessor/steps'; +import { operators } from '@console/dev-console/integration-tests/support/constants'; +import { + createGitWorkloadIfNotExistsOnTopologyPage, + projectNameSpace, + topologyHelper, + topologyPage, + verifyAndInstallOperator, +} from '@console/dev-console/integration-tests/support/pages'; +import { topologyAddOptionsPO } from '../../page-objects/topology-add-options-po'; + +Given('user has installed OpenShift Serverless Operator', () => { + verifyAndInstallOperator(operators.ServerlessOperator); +}); + +Given('user has created or selected namespace {string}', (projectName: string) => { + Cypress.env('NAMESPACE', projectName); + projectNameSpace.selectOrCreateProject(`${projectName}`); +}); + +Given( + 'user has created {string} workload in {string} application', + (nodeName: string, appName: string) => { + createGitWorkloadIfNotExistsOnTopologyPage( + 'https://github.com/sclorg/nodejs-ex.git', + nodeName, + 'Deployment', + appName, + ); + topologyHelper.verifyWorkloadInTopologyPage(nodeName); + }, +); + +When('user right clicks on empty graph view', () => { + cy.byLegacyTestID('topology').rightclick(10, 10); +}); + +When('user hovers on Add to Project', () => { + cy.get('.odc-topology-context-menu').trigger('mouseover'); +}); + +Then( + 'user can see in context options {string}, {string}, {string}, {string}, {string}, {string}, {string}, {string}, {string}', + ( + s1: string, + s2: string, + s3: string, + s4: string, + s5: string, + s6: string, + s7: string, + s8: string, + s9: string, + ) => { + cy.get(topologyAddOptionsPO(s1)).should('be.visible'); + cy.get(topologyAddOptionsPO(s2)).should('be.visible'); + cy.get(topologyAddOptionsPO(s3)).should('be.visible'); + cy.get(topologyAddOptionsPO(s4)).should('be.visible'); + cy.get(topologyAddOptionsPO(s5)).should('be.visible'); + cy.get(topologyAddOptionsPO(s6)).should('be.visible'); + cy.get(topologyAddOptionsPO(s7)).should('be.visible'); + cy.get(topologyAddOptionsPO(s8)).should('be.visible'); + cy.get(topologyAddOptionsPO(s9)).should('be.visible'); + }, +); + +When('user right clicks on Application Grouping {string}', (appName: string) => { + topologyPage.rightClickOnApplicationGroupings(appName); +}); + +When('user hovers on Add to application', () => { + cy.get('.odc-topology-context-menu') + .contains('Add to application') + .trigger('mouseover'); +}); + +Then( + 'user can see in context options {string}, {string}, {string}, {string}', + (s1: string, s2: string, s3: string, s4: string) => { + cy.get(topologyAddOptionsPO(s1)).should('be.visible'); + cy.get(topologyAddOptionsPO(s2)).should('be.visible'); + cy.get(topologyAddOptionsPO(s3)).should('be.visible'); + cy.get(topologyAddOptionsPO(s4)).should('be.visible'); + }, +); + +When('user clicks on Delete application', () => { + cy.get('.odc-topology-context-menu') + .contains('Delete application') + .click(); +}); + +When( + 'user enters the name {string} in the Delete application modal and clicks on Delete button', + (appName: string) => { + topologyPage.deleteApplication(appName); + }, +); + +Then("user won't be able to see the {string} Application Groupings", (appName: string) => { + topologyPage.verifyApplicationGroupingsDeleted(appName); +}); diff --git a/integration-tests/support/step-definitions/topology/topology-filters.ts b/integration-tests/support/step-definitions/topology/topology-filters.ts new file mode 100644 index 0000000..20b0fb5 --- /dev/null +++ b/integration-tests/support/step-definitions/topology/topology-filters.ts @@ -0,0 +1,131 @@ +import { When, Then } from 'cypress-cucumber-preprocessor/steps'; +import { topologyPage } from '@console/topology/integration-tests/support/pages/topology/topology-page'; +import { topologyPO } from '../../page-objects/topology-po'; + +When('user clicks on the Display dropdown', () => { + topologyPage.clickDisplayOptionDropdown(); +}); + +Then('user will see the Expand is checked', () => { + topologyPage.verifyExpandChecked(); +}); + +Then('user will see the Pod count is unchecked', () => { + topologyPage.verifyPodCountUnchecked(); +}); + +Then('user will see the Labels is checked', () => { + cy.get(topologyPO.graph.displayOptions.showLabels).should('be.checked'); +}); + +Then('app icon is not displayed', () => { + topologyPage.clickDisplayOptionDropdown(); + cy.byTestID('icon application').should('not.exist'); +}); + +Then('user will see the Application groupings option is disabled', () => { + cy.get(topologyPO.graph.displayOptions.applicationGroupings).should('be.disabled'); +}); + +Then('user will see that the Expand options are disabled', () => { + topologyPage.verifyExpandOptionsDisabled(); + topologyPage.defaultState(); +}); + +When('user unchecks the Expand', () => { + topologyPage.uncheckExpandToggle(); +}); + +Then('user will see the Knative Services checkbox is disabled', () => { + topologyPage.verifyExpandDisabled(); +}); + +Then('user will see Deployment and DeploymentConfig options with 1 associated with it', () => { + cy.get(topologyPO.toolbarFilterPO.deployment).should('be.visible'); + cy.get(topologyPO.toolbarFilterPO.deploymentConfig).should('be.visible'); + cy.get(topologyPO.toolbarFilterPO.deploymentSpan).contains('Deployment (1)'); + cy.get(topologyPO.toolbarFilterPO.deploymentConfigSpan).contains('DeploymentConfig (1)'); +}); + +Then('user clicks on Deployment checkbox to see only the deployment type workload', () => { + cy.get(topologyPO.toolbarFilterPO.deploymentCheckbox).check(); + cy.get(topologyPO.toolbarFilterPO.deploymentApp).should('be.visible'); + cy.get(topologyPO.toolbarFilterPO.deploymentConfigApp).should('not.exist'); +}); + +Then( + 'user clicks on DeploymentConfig checkbox to see only the deploymentconfig type workload', + () => { + cy.get(topologyPO.toolbarFilterPO.deploymentCheckbox).uncheck(); + cy.get(topologyPO.toolbarFilterPO.deploymentConfigCheckbox).check(); + cy.get(topologyPO.toolbarFilterPO.deploymentConfigApp).should('be.visible'); + cy.get(topologyPO.toolbarFilterPO.deploymentApp).should('not.exist'); + }, +); +When('user clicks display options', () => { + cy.get(topologyPO.displayFilter.display).click(); +}); + +When('user disbales expand option', () => { + // eslint-disable-next-line promise/catch-or-return + cy.get('body').then((el) => { + if (el.find(topologyPO.displayFilter.disabledClass).length === 0) { + cy.get(topologyPO.displayFilter.expandOption).click({ force: true }); + } + }); +}); + +When('user enables expand option', () => { + // eslint-disable-next-line promise/catch-or-return + cy.get('body').then((el) => { + if (el.find(topologyPO.displayFilter.disabledClass).length !== 0) { + cy.get(topologyPO.displayFilter.expandOption).click({ force: true }); + } + }); +}); + +When('user unchecks the application grouping option', () => { + cy.get(topologyPO.displayFilter.applicationGroupingOption).uncheck({ force: true }); +}); + +When('user checks the application grouping option', () => { + cy.get(topologyPO.displayFilter.applicationGroupingOption).check({ force: true }); +}); + +Then('user will see the application_groupings checkbox will disable', () => { + cy.get(topologyPO.displayFilter.applicationGroupingOption).should('be.disabled'); +}); + +Then('user will see workload in text view', () => { + cy.get(topologyPO.displayFilter.unexpandedNode).should('not.exist'); +}); + +When('user checks the pod count option', () => { + cy.get(topologyPO.displayFilter.podLabelOptions) + .eq(2) + .check(); +}); + +Then('user will able to see the pod count inside workload', () => { + cy.get(topologyPO.displayFilter.podRingText).should('be.visible'); +}); + +When('user unchecks the labels option', () => { + cy.get(topologyPO.displayFilter.podLabelOptions) + .eq(3) + .uncheck(); +}); + +Then('user will not able to see the labels on workload', () => { + cy.get(topologyPO.graph.nodeLabel).should('not.exist'); + cy.get(topologyPO.graph.groupLabel).should('not.exist'); +}); + +Then('user will see deployment section is not visible', () => { + cy.get(topologyPO.displayFilter.deploymentLabel).should('not.exist'); +}); + +Then('user will see deployments in count view', () => { + cy.get(topologyPO.displayFilter.deployemntCount).should('be.visible'); + topologyPage.defaultState(); +}); diff --git a/integration-tests/support/step-definitions/topology/workload-sidebar.ts b/integration-tests/support/step-definitions/topology/workload-sidebar.ts new file mode 100644 index 0000000..635279b --- /dev/null +++ b/integration-tests/support/step-definitions/topology/workload-sidebar.ts @@ -0,0 +1,166 @@ +import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps'; +import { devNavigationMenu } from '@console/dev-console/integration-tests/support/constants'; +import { + app, + createGitWorkload, + navigateTo, + projectNameSpace, + topologyActions, +} from '@console/dev-console/integration-tests/support/pages'; +import { + topologyHelper, + topologyPage, + topologySidePane, +} from '@console/topology/integration-tests/support/pages/topology'; +import { topologyPO } from '../../page-objects/topology-po'; +import { checkPodsText } from '../../pages/functions/add-secret'; + +Given('user has scaled up the pod to 2 for workload {string}', (workloadName: string) => { + topologyPage.componentNode(workloadName).click({ force: true }); + topologySidePane.selectTab('Details'); + topologySidePane.scaleUpPodCount(); +}); + +When('user scales up the pod', () => { + topologySidePane.scaleUpPodCount(); +}); + +When('user scales down the pod number', () => { + topologySidePane.scaleDownPodCount(); +}); + +Then( + 'user is able to see pod Scaling to {string} for workload {string}', + (podText: string, workloadName: string) => { + topologySidePane.close(); + cy.reload(); + app.waitForDocumentLoad(); + topologyPage.componentNode(workloadName).click({ force: true }); + topologySidePane.selectTab('Details'); + topologySidePane.verifyPodText(podText); + }, +); + +Given('user has created a deployment workload {string}', (componentName: string) => { + navigateTo(devNavigationMenu.Add); + createGitWorkload( + 'https://github.com/sclorg/nodejs-ex.git', + componentName, + 'Deployment', + 'nodejs-ex-git-app', + ); +}); + +Given('user is at Topology chart view', () => { + navigateTo(devNavigationMenu.Topology); + cy.get(topologyPO.switcher).should('have.attr', 'aria-label', 'List view'); +}); + +When('user clicks on workload {string} to open sidebar', (workloadName: string) => { + topologyPage.componentNode(workloadName).click({ force: true }); +}); + +When('user clicks on Action menu', () => { + topologySidePane.clickActionsDropDown(); +}); + +When('user clicks {string} from action menu', (actionItem: string) => { + app.waitForLoad(); + topologyActions.selectAction(actionItem); +}); + +When('user deletes the existing annotation for route', () => { + cy.get(topologyPO.deploymentStrategy.envRow) + .eq(1) + .within(() => { + cy.get('[data-test="delete-button"]').click(); + }); +}); + +When('user enters key as {string}', (annotationKey: string) => { + cy.get(topologyPO.graph.addNewAnnotations).click(); + cy.get(topologyPO.deploymentStrategy.envName) + .last() + .clear() + .type(annotationKey); +}); + +When('user enters value as {string}', (annotationValue: string) => { + cy.get(topologyPO.deploymentStrategy.envValue) + .last() + .clear() + .type(annotationValue); + cy.get(topologyPO.graph.deleteWorkload).click(); +}); + +Then('user can see route decorator has been hidden for workload {string}', (appName: string) => { + cy.reload(); + app.waitForDocumentLoad(); + topologyHelper.search(appName); + cy.get(topologyPO.graph.reset).click(); + cy.get(topologyPO.highlightNode) + .should('be.visible') + .within(() => { + cy.get(topologyPO.graph.routeDecorator).should('not.exist'); + }); +}); + +Then('user can see the new route href in route decorator be {string}', (value: string) => { + cy.reload(); + app.waitForDocumentLoad(); + topologyHelper.search('nodejs-ex-2'); + cy.get(topologyPO.graph.reset).click(); + cy.get(topologyPO.highlightNode) + .should('be.visible') + .within(() => { + cy.get(topologyPO.graph.routeDecorator, { timeout: 120000 }).should( + 'have.attr', + 'href', + value, + ); + }); +}); + +Given('user has selected namespace {string}', (projectName: string) => { + projectNameSpace.selectProject(projectName); +}); + +When('user clicks on knative workload {string}', (workloadName: string) => { + topologyPage.waitForLoad(); + topologyPage.knativeNode(workloadName).click({ force: true }); +}); + +When('user clicks on sidebar workload {string}', (workloadName: string) => { + topologyPage.waitForLoad(); + topologyPage.componentNode(workloadName).click({ force: true }); +}); + +When('user opens Details tab', () => { + topologyPage.waitForLoad(); + topologyPage.componentNode('nodejs-ex-git-1').click({ force: true }); + topologySidePane.selectTab('Details'); +}); + +When('user can see pod count is 1', () => { + topologySidePane.close(); + topologyPage.componentNode('nodejs-ex-git-1').click({ force: true }); + checkPodsText(); +}); + +When('user can see workload {string} is created', (workloadName: string) => { + navigateTo(devNavigationMenu.Topology); + topologyPage.componentNode(workloadName).should('be.visible'); +}); + +When('user clicks on {string} from action menu', (actionItem: string) => { + topologyActions.selectAction(actionItem); +}); + +Then('user can see traffic details for pod', () => { + topologySidePane.selectTab('Resources'); + topologySidePane.verifySection('Pods'); + cy.get(topologyPO.sidePane.resourcesTab.waitingPods, { timeout: 100000 }).should('not.exist'); + cy.get(topologyPO.sidePane.resourcesTab.podTrafficStatus, { timeout: 100000 }).should( + 'be.visible', + ); +}); diff --git a/integration-tests/support/test-data/bindable-resource1.yaml b/integration-tests/support/test-data/bindable-resource1.yaml new file mode 100644 index 0000000..e03fd6f --- /dev/null +++ b/integration-tests/support/test-data/bindable-resource1.yaml @@ -0,0 +1,35 @@ +apiVersion: binding.operators.coreos.com/v1alpha1 +kind: BindableKinds +metadata: + creationTimestamp: '2021-11-18T05:19:15Z' + generation: 6 + managedFields: + - apiVersion: binding.operators.coreos.com/v1alpha1 + fieldsType: FieldsV1 + fieldsV1: + 'f:status': {} + manager: manager + operation: Update + time: '2021-11-18T05:19:15Z' + name: bindable-kinds + resourceVersion: '29894' + uid: ae6df662-41a2-45da-9acf-1332d5c69adf +status: + - group: rabbitmq.com + kind: RabbitmqCluster + version: v1beta1 + - group: rhoas.redhat.com + kind: KafkaConnection + version: v1alpha1 + - group: redis.redis.opstreelabs.in + kind: Redis + version: v1beta1 + - group: postgres-operator.crunchydata.com + kind: PostgresCluster + version: v1beta1 + - group: postgresql.k8s.enterprisedb.io + kind: Cluster + version: v1 + - group: rhoas.redhat.com + kind: ServiceRegistryConnection + version: v1alpha1 \ No newline at end of file diff --git a/integration-tests/support/test-data/hippo-postgres-cluster.yaml b/integration-tests/support/test-data/hippo-postgres-cluster.yaml new file mode 100644 index 0000000..1ee09cf --- /dev/null +++ b/integration-tests/support/test-data/hippo-postgres-cluster.yaml @@ -0,0 +1,38 @@ +apiVersion: postgres-operator.crunchydata.com/v1beta1 +kind: PostgresCluster +metadata: + name: hippo +spec: + image: registry.developers.crunchydata.com/crunchydata/crunchy-postgres-ha:centos8-13.4-0 + postgresVersion: 13 + instances: + - name: instance1 + dataVolumeClaimSpec: + accessModes: + - "ReadWriteOnce" + resources: + requests: + storage: 1Gi + backups: + pgbackrest: + image: registry.developers.crunchydata.com/crunchydata/crunchy-pgbackrest:centos8-2.33-2 + repos: + - name: repo1 + volume: + volumeClaimSpec: + accessModes: + - "ReadWriteOnce" + resources: + requests: + storage: 1Gi + - name: repo2 + volume: + volumeClaimSpec: + accessModes: + - "ReadWriteOnce" + resources: + requests: + storage: 1Gi + proxy: + pgBouncer: + image: registry.developers.crunchydata.com/crunchydata/crunchy-pgbouncer:centos8-1.15-2 diff --git a/integration-tests/support/test-data/postgres-operator-backed.yaml b/integration-tests/support/test-data/postgres-operator-backed.yaml new file mode 100644 index 0000000..6b6031d --- /dev/null +++ b/integration-tests/support/test-data/postgres-operator-backed.yaml @@ -0,0 +1,48 @@ +apiVersion: postgres-operator.crunchydata.com/v1beta1 +kind: PostgresCluster +metadata: + name: postgres-operator-backed + namespace: aut-topology-delete-workload +spec: + port: 5432 + dataSource: + pgbackrest: + stanza: db + repo: + name: repo1 + standby: + enabled: true + repoName: repo1 + userInterface: + pgAdmin: + replicas: 1 + dataVolumeClaimSpec: + accessModes: + - "read" + proxy: + pgBouncer: + port: 5432 + replicas: 1 + backups: + pgbackrest: + restore: + enabled: false + repoName: repo1 + repos: + - name: repo1 + patroni: + leaderLeaseDurationSeconds: 30 + port: 8008 + switchover: + type: Switchover + enabled: false + syncPeriodSeconds: 10 + instances: + - dataVolumeClaimSpec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi + replicas: 1 + postgresVersion: 13 \ No newline at end of file diff --git a/integration-tests/support/test-data/redis-standalone.yaml b/integration-tests/support/test-data/redis-standalone.yaml new file mode 100644 index 0000000..c362cab --- /dev/null +++ b/integration-tests/support/test-data/redis-standalone.yaml @@ -0,0 +1,23 @@ +kind: Redis +apiVersion: redis.redis.opstreelabs.in/v1beta1 +metadata: + name: redis-standalone +spec: + kubernetesConfig: + image: 'quay.io/opstree/redis:v7.0.5' + imagePullPolicy: IfNotPresent + redisSecret: + key: test + name: test + storage: + volumeClaimTemplate: + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi + redisExporter: + enabled: true + image: 'quay.io/opstree/redis-exporter:v1.44.0' + imagePullPolicy: IfNotPresent \ No newline at end of file diff --git a/integration-tests/support/test-data/servicebinding-resource-label-selector.yaml b/integration-tests/support/test-data/servicebinding-resource-label-selector.yaml new file mode 100644 index 0000000..d2cc865 --- /dev/null +++ b/integration-tests/support/test-data/servicebinding-resource-label-selector.yaml @@ -0,0 +1,17 @@ +apiVersion: binding.operators.coreos.com/v1alpha1 +kind: ServiceBinding +metadata: + name: test-connector4 +spec: + services: + - group: redis.redis.opstreelabs.in + kind: Redis + name: redis-standalone + version: v1beta1 + application: + labelSelector: + matchLabels: + app: node-ej + group: apps + version: v1 + resource: deployments \ No newline at end of file diff --git a/integration-tests/testData/resource-quota/resource-quota.yaml b/integration-tests/testData/resource-quota/resource-quota.yaml new file mode 100644 index 0000000..0020a2d --- /dev/null +++ b/integration-tests/testData/resource-quota/resource-quota.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: ResourceQuota +metadata: + name: resourcequota1 +spec: + hard: + pods: '1' +--- +apiVersion: v1 +kind: ResourceQuota +metadata: + name: resourcequota2 +spec: + hard: + pods: '1' \ No newline at end of file diff --git a/integration-tests/tsconfig.json b/integration-tests/tsconfig.json new file mode 100644 index 0000000..7b93377 --- /dev/null +++ b/integration-tests/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../dev-console/integration-tests/tsconfig.json", + "compilerOptions": { + "target": "es5", + "module": "commonjs", + "strict": true, + "esModuleInterop": true, + "types": ["cypress"], + "lib": ["es6", "dom", "es2017"] + }, + "include": ["**/*.ts", "./support/commands/index.ts"] +} diff --git a/jest.config.ts b/jest.config.ts new file mode 100644 index 0000000..6e26701 --- /dev/null +++ b/jest.config.ts @@ -0,0 +1,41 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +import { pathsToModuleNameMapper } from 'ts-jest'; + +import type { Config } from '@jest/types'; +const { compilerOptions } = require('./tsconfig'); + +// Sync object +const config: Config.InitialOptions = { + testEnvironment: 'jest-environment-jsdom', + preset: 'ts-jest', + moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx'], + transform: { + '^.+\\.[t|j]sx?$': 'ts-jest', + }, + testURL: 'http://localhost/', + testRegex: '.*\\.test\\.(ts|tsx|js|jsx)$', + testPathIgnorePatterns: ['/node_modules/', '/dist/'], + transformIgnorePatterns: [ + '/node_modules/(?!(@kubevirt-ui/kubevirt-api|byte-size|@patternfly|@openshift-console\\S*?)/.*)', + ], + setupFilesAfterEnv: ['/jest-setup.ts'], + moduleNameMapper: { + '\\.(css|less|scss|svg)$': '/__mocks__/dummy.ts', + '@console/*': '/__mocks__/dummy.ts', + 'react-i18next': '/__mocks__/react-i18next.ts', + 'react-router-dom': '/__mocks__/react-router-dom.ts', + ...pathsToModuleNameMapper(compilerOptions.paths, { prefix: '/' }), + }, + globals: { + SERVER_FLAGS: { + basePath: 'http://localhost:9000/', + }, + 'ts-jest': { + isolatedModules: true, + diagnostics: { + ignoreCodes: ['TS151001'], + }, + }, + }, +}; +export default config; diff --git a/locales/en/expNamespace.json b/locales/en/expNamespace.json new file mode 100644 index 0000000..cc5194d --- /dev/null +++ b/locales/en/expNamespace.json @@ -0,0 +1,3 @@ +{ + "All the resources are exported successfully from {{namespace}}. Click below to download it.": "All the resources are exported successfully from {{namespace}}. Click below to download it." +} diff --git a/locales/en/plugin__topology-plugin.json b/locales/en/plugin__topology-plugin.json new file mode 100644 index 0000000..074106c --- /dev/null +++ b/locales/en/plugin__topology-plugin.json @@ -0,0 +1,338 @@ +{ + " or <1>create a Project": " or <1>create a Project", + "{{count}} annotation_one": "{{count}} annotation", + "{{count}} annotation_other": "{{count}} annotation", + "{{count}} day_one": "{{count}} day", + "{{count}} day_other": "{{count}} day", + "{{count}} hour_one": "{{count}} hour", + "{{count}} hour_other": "{{count}} hour", + "{{count}} minute_one": "{{count}} minute", + "{{count}} minute_other": "{{count}} minute", + "{{count}} resource reached quota_one": "{{count}} resource reached quota", + "{{count}} resource reached quota_other": "{{count}} resource reached quota", + "{{count}} second_one": "{{count}} second", + "{{count}} second_other": "{{count}} second", + "{{count}} toleration_one": "{{count}} toleration", + "{{count}} toleration_other": "{{count}} toleration", + "{{description}} for": "{{description}} for", + "{{groupLabel}} {{resourceLabel}}": "{{groupLabel}} {{resourceLabel}}", + "{{kindLabel}} {{label}}": "{{kindLabel}} {{label}}", + "{{label}} sub-resources": "{{label}} sub-resources", + "{{labels}} content is not available in the catalog at this time due to loading failures.": "{{labels}} content is not available in the catalog at this time due to loading failures.", + "{{length}} Pods": "{{length}} Pods", + "{{maxSurge}} greater than {{count}} pod_one": "{{maxSurge}} greater than {{count}} pod", + "{{maxSurge}} greater than {{count}} pod_other": "{{maxSurge}} greater than {{count}} pod", + "{{maxUnavailable}} of {{count}} pod_one": "{{maxUnavailable}} of {{count}} pod", + "{{maxUnavailable}} of {{count}} pod_other": "{{maxUnavailable}} of {{count}} pod", + "{{metricLabel}} usage by Pod": "{{metricLabel}} usage by Pod", + "{{ready, number}} of {{count, number}} Pod_one": "{{ready, number}} of {{count, number}} Pod", + "{{ready, number}} of {{count, number}} Pod_other": "{{ready, number}} of {{count, number}} Pod", + "{{resourceLabel}} sub-resources": "{{resourceLabel}} sub-resources", + "<0>Start building your application or visit the <2>Add page for more details.": "<0>Start building your application or visit the <2>Add page for more details.", + "~Edit source code": "~Edit source code", + "404: Not Found": "404: Not Found", + "A unique name given to the application grouping to label your resources.": "A unique name given to the application grouping to label your resources.", + "Access context menu": "Access context menu", + "Access create connector handle": "Access create connector handle", + "Actions": "Actions", + "Active": "Active", + "Active deadline seconds": "Active deadline seconds", + "Add health checks": "Add health checks", + "Add resources": "Add resources", + "Add to Project": "Add to Project", + "Alerts:": "Alerts:", + "All applications": "All applications", + "Always restart": "Always restart", + "An error occurred": "An error occurred", + "An error occurred. Please try again.": "An error occurred. Please try again.", + "Annotations": "Annotations", + "Application": "Application", + "Application export in <1>{{namespace}} is in progress.": "Application export in <1>{{namespace}} is in progress.", + "Application export in <1>{{namespace}} is in progress. Started at {{startTime}}.": "Application export in <1>{{namespace}} is in progress. Started at {{startTime}}.", + "Application groupings": "Application groupings", + "Application name": "Application name", + "Are you sure you want to move <1>{{nodeLabel}} from {{sourceLabel}} to {{targetLabel}}?": "Are you sure you want to move <1>{{nodeLabel}} from {{sourceLabel}} to {{targetLabel}}?", + "Are you sure you want to remove <1>{{nodeLabel}} from {{sourceLabel}}?": "Are you sure you want to remove <1>{{nodeLabel}} from {{sourceLabel}}?", + "Bindable service": "Bindable service", + "Broker": "Broker", + "Brokers": "Brokers", + "Build {{status}}": "Build {{status}}", + "Build <1> encountered an error": "Build <1> encountered an error", + "Build <1> failed": "Build <1> failed", + "Build <1> is {build.status?.phase?.toLowerCase()}": "Build <1> is {build.status?.phase?.toLowerCase()}", + "Build <1> is new": "Build <1> is new", + "Build <1> is pending": "Build <1> is pending", + "Build <1> is running": "Build <1> is running", + "Build <1> was cancelled": "Build <1> was cancelled", + "Build <1> was complete": "Build <1> was complete", + "Builds": "Builds", + "Builds:": "Builds:", + "Can not create a connection from a node to itself.": "Can not create a connection from a node to itself.", + "Cancel": "Cancel", + "Cancel Export": "Cancel Export", + "Cannot do a contextual binding without a source": "Cannot do a contextual binding without a source", + "Cannot find resource ({{contextualSource}}) to do a contextual binding to": "Cannot find resource ({{contextualSource}}) to do a contextual binding to", + "Clear all filters": "Clear all filters", + "Click": "Click", + "Close": "Close", + "Collapse groups": "Collapse groups", + "Component trace:": "Component trace:", + "Concurrency policy": "Concurrency policy", + "Configuration": "Configuration", + "Configurations": "Configurations", + "Connect <1>{{sourceLabel}} to": "Connect <1>{{sourceLabel}} to", + "Connect <1>{{sourceName}} to service <3>{{targetName}}.": "Connect <1>{{sourceName}} to service <3>{{targetName}}.", + "Connections": "Connections", + "Container {{containersName}} does not have health checks to ensure your application is running correctly.": "Container {{containersName}} does not have health checks to ensure your application is running correctly.", + "Continue": "Continue", + "Copied": "Copied", + "Copy to clipboard": "Copy to clipboard", + "CPU": "CPU", + "CPU limit must be greater than or equal to request.": "CPU limit must be greater than or equal to request.", + "CPU request must be less than or equal to limit.": "CPU request must be less than or equal to limit.", + "CrashLoopBackOff indicates that the application within the container is failing to start properly.": "CrashLoopBackOff indicates that the application within the container is failing to start properly.", + "Create": "Create", + "Create a visual connector": "Create a visual connector", + "Create application": "Create application", + "Create Service Binding": "Create Service Binding", + "Created at": "Created at", + "Current count": "Current count", + "Debug container {{name}}": "Debug container {{name}}", + "Decrease the Pod count": "Decrease the Pod count", + "Delete": "Delete", + "Delete connector": "Delete connector", + "Delete Connector?": "Delete Connector?", + "Deleting the visual connector removes the `connects-to` annotation from the resources. Are you sure you want to delete the visual connector?": "Deleting the visual connector removes the `connects-to` annotation from the resources. Are you sure you want to delete the visual connector?", + "Description:": "Description:", + "Desired completions": "Desired completions", + "Desired count": "Desired count", + "Details": "Details", + "Display options": "Display options", + "Do you want to export your application?": "Do you want to export your application?", + "DomainMapping": "DomainMapping", + "DomainMappings": "DomainMappings", + "Download": "Download", + "Drag": "Drag", + "Drag + Drop": "Drag + Drop", + "Drop file ({{fileTypes}}) here": "Drop file ({{fileTypes}}) here", + "Edit": "Edit", + "Edit application grouping": "Edit application grouping", + "Edit source code": "Edit source code", + "Edit update strategy": "Edit update strategy", + "Error": "Error", + "Error creating connection": "Error creating connection", + "Error details": "Error details", + "Error loading - {{placeholder}}": "Error loading - {{placeholder}}", + "Error Loading {{label}}": "Error Loading {{label}}", + "Error Loading {{label}}: {{message}}": "Error Loading {{label}}: {{message}}", + "Error moving connection": "Error moving connection", + "Error: invalid resource kind provided for updating application.": "Error: invalid resource kind provided for updating application.", + "Error: no resource provided to update application for.": "Error: no resource provided to update application for.", + "Event sink connector": "Event sink connector", + "Event source connector": "Event source connector", + "Expand": "Expand", + "Expand groups": "Expand groups", + "Export application": "Export application", + "Export Application": "Export Application", + "Export of resources in <1>{{namespace}} has failed with error: {error.message}": "Export of resources in <1>{{namespace}} has failed with error: {error.message}", + "Export of resources in <1>{{namespace}} has started.": "Export of resources in <1>{{namespace}} has started.", + "Filter by resource": "Filter by resource", + "Find by label...": "Find by label...", + "Find by name": "Find by name", + "Find by name...": "Find by name...", + "Fit to screen": "Fit to screen", + "Go to Search": "Go to Search", + "Graph view": "Graph view", + "Health checks": "Health checks", + "Helm Release": "Helm Release", + "Hide details": "Hide details", + "Host IP": "Host IP", + "Hover": "Hover", + "If the container exits with a non-zero status code, restart it.": "If the container exits with a non-zero status code, restart it.", + "If the container restarts for any reason, restart it. Useful for stateless services that may fail from time to time.": "If the container restarts for any reason, restart it. Useful for stateless services that may fail from time to time.", + "Image pull secret": "Image pull secret", + "Increase the Pod count": "Increase the Pod count", + "Integration": "Integration", + "Integrations": "Integrations", + "Interval": "Interval", + "Just now": "Just now", + "Kafka": "Kafka", + "Kafka connector": "Kafka connector", + "Kafkas": "Kafkas", + "KafkaSink": "KafkaSink", + "KafkaSinks": "KafkaSinks", + "KafkaTopic": "KafkaTopic", + "KafkaTopics": "KafkaTopics", + "Kamelet": "Kamelet", + "KameletBinding": "KameletBinding", + "KameletBindings": "KameletBindings", + "Kamelets": "Kamelets", + "Kiali": "Kiali", + "Kiali Graph view": "Kiali Graph view", + "Kiali link": "Kiali link", + "KnativeServing": "KnativeServing", + "KnativeServings": "KnativeServings", + "Label": "Label", + "Labels": "Labels", + "Labels for": "Labels for", + "Labels help you organize and select resources. Adding labels below will let you query for objects that have similar, overlapping or dissimilar labels.": "Labels help you organize and select resources. Adding labels below will let you query for objects that have similar, overlapping or dissimilar labels.", + "Last schedule time": "Last schedule time", + "Latest version": "Latest version", + "Limit must be greater than or equal to 0.": "Limit must be greater than or equal to 0.", + "List view": "List view", + "Loading is taking longer than expected": "Loading is taking longer than expected", + "Location:": "Location:", + "Logs not available yet": "Logs not available yet", + "Managed by": "Managed by", + "Max surge": "Max surge", + "Max unavailable": "Max unavailable", + "Max unavailable {{maxUnavailable}} of {{count}} pod_one": "Max unavailable {{maxUnavailable}} of {{count}} pod", + "Max unavailable {{maxUnavailable}} of {{count}} pod_other": "Max unavailable {{maxUnavailable}} of {{count}} pod", + "Memory limit must be greater than or equal to request.": "Memory limit must be greater than or equal to request.", + "Memory request must be less than or equal to limit.": "Memory request must be less than or equal to limit.", + "Message": "Message", + "Min available {{minAvailable}} of {{count}} pod_one": "Min available {{minAvailable}} of {{count}} pod", + "Min available {{minAvailable}} of {{count}} pod_other": "Min available {{minAvailable}} of {{count}} pod", + "Min ready seconds": "Min ready seconds", + "Monitoring alert": "Monitoring alert", + "Move": "Move", + "Move component node": "Move component node", + "Move connector": "Move connector", + "Name": "Name", + "Namespace": "Namespace", + "Never restart": "Never restart", + "Never restart the container. Useful for containers that exit when they have completed a specific job, like a data import daemon.": "Never restart the container. Useful for containers that exit when they have completed a specific job, like a data import daemon.", + "No {{label}} found": "No {{label}} found", + "No {{metricLabel}} metrics available.": "No {{metricLabel}} metrics available.", + "No application group": "No application group", + "No bindable services available": "No bindable services available", + "No Builds found for this Build Config.": "No Builds found for this Build Config.", + "No Jobs found for this resource.": "No Jobs found for this resource.", + "No labels": "No labels", + "No owner": "No owner", + "No PodDisruptionBudget": "No PodDisruptionBudget", + "No resources found": "No resources found", + "No Routes found for this resource.": "No Routes found for this resource.", + "No selector": "No selector", + "No Services found for this resource.": "No Services found for this resource.", + "Node": "Node", + "Node selector": "Node selector", + "Not all Containers have health checks to ensure your application is running correctly.": "Not all Containers have health checks to ensure your application is running correctly.", + "Not configured": "Not configured", + "Not found": "Not found", + "Not Receiving Traffic": "Not Receiving Traffic", + "Oh no! Something went wrong.": "Oh no! Something went wrong.", + "OK": "OK", + "Open quick search modal": "Open quick search modal", + "Open URL": "Open URL", + "Operator groupings": "Operator groupings", + "Owner": "Owner", + "Parallelism": "Parallelism", + "plugin__topology-pluginCreate Service Binding": "plugin__topology-pluginCreate Service Binding", + "plugin__topology-pluginEdit {{kind}}": "plugin__topology-pluginEdit {{kind}}", + "plugin__topology-pluginEdit resource limits": "plugin__topology-pluginEdit resource limits", + "plugin__topology-pluginEdit update strategy": "plugin__topology-pluginEdit update strategy", + "plugin__topology-pluginPause rollouts": "plugin__topology-pluginPause rollouts", + "plugin__topology-pluginRestart rollout": "plugin__topology-pluginRestart rollout", + "plugin__topology-pluginResume rollouts": "plugin__topology-pluginResume rollouts", + "plugin__topology-pluginStart rollout": "plugin__topology-pluginStart rollout", + "Pod": "Pod", + "Pod count": "Pod count", + "Pod crash loop back-off": "Pod crash loop back-off", + "Pod IP": "Pod IP", + "Pod port:": "Pod port:", + "Pod selector": "Pod selector", + "Pod unschedulable": "Pod unschedulable", + "PodDisruptionBudget": "PodDisruptionBudget", + "PodDisruptionBudgets": "PodDisruptionBudgets", + "Pods": "Pods", + "Prerequisites": "Prerequisites", + "Progress deadline seconds": "Progress deadline seconds", + "Quick search button": "Quick search button", + "Quick Starts": "Quick Starts", + "Receiving Traffic": "Receiving Traffic", + "Remove": "Remove", + "Remove component node from application": "Remove component node from application", + "Request must be greater than or equal to 0.": "Request must be greater than or equal to 0.", + "Required": "Required", + "Reset view": "Reset view", + "Resource Quotas": "Resource Quotas", + "Resources": "Resources", + "Restart Export": "Restart Export", + "Restart on failure": "Restart on failure", + "Restart policy": "Restart policy", + "Restricted Access": "Restricted Access", + "Revision": "Revision", + "Revisions": "Revisions", + "Right click": "Right click", + "Rollout in progress...": "Rollout in progress...", + "Route": "Route", + "Routes": "Routes", + "Runtime class": "Runtime class", + "Save": "Save", + "Schedule": "Schedule", + "Search results may appear outside of the visible area. <2>Click here to fit to the screen.": "Search results may appear outside of the visible area. <2>Click here to fit to the screen.", + "second_one": "second", + "second_other": "second", + "Secret": "Secret", + "Select a Project to view the topology<1>.": "Select a Project to view the topology<1>.", + "Select a service to connect to.": "Select a service to connect to.", + "Select an application": "Select an application", + "Select an Application group to add the component <2>{{resourceName}} to": "Select an Application group to add the component <2>{{resourceName}} to", + "Select an Application to group this component.": "Select an Application to group this component.", + "Select Service": "Select Service", + "Service": "Service", + "Service binding already exists. Select a different service to connect to.": "Service binding already exists. Select a different service to connect to.", + "Service port:": "Service port:", + "ServiceBinding": "ServiceBinding", + "ServiceBindings": "ServiceBindings", + "Services": "Services", + "Shortcuts": "Shortcuts", + "Show": "Show", + "Show details": "Show details", + "Stack trace:": "Stack trace:", + "Start": "Start", + "Start Build": "Start Build", + "Starting deadline seconds": "Starting deadline seconds", + "Status": "Status", + "Subscription": "Subscription", + "Subscriptions": "Subscriptions", + "Timed out fetching new data. The data below is stale.": "Timed out fetching new data. The data below is stale.", + "Timeout": "Timeout", + "To create a Service binding, first create a bindable service.": "To create a Service binding, first create a bindable service.", + "To troubleshoot, view logs and events, then debug in terminal.": "To troubleshoot, view logs and events, then debug in terminal.", + "Tolerations": "Tolerations", + "Topology": "Topology", + "Traffic connector": "Traffic connector", + "Traffic distribution connector": "Traffic distribution connector", + "Trigger": "Trigger", + "Triggers": "Triggers", + "Unable to create kafka connector as bootstrapServerHost or secret is not available in target resource.": "Unable to create kafka connector as bootstrapServerHost or secret is not available in target resource.", + "Unable to move connector of type {{type}}.": "Unable to move connector of type {{type}}.", + "Unable to retrieve model for: {{kind}}": "Unable to retrieve model for: {{kind}}", + "Unable to update application, invalid resource type: {{kind}}": "Unable to update application, invalid resource type: {{kind}}", + "unassigned": "unassigned", + "Unit must be Mi or Gi.": "Unit must be Mi or Gi.", + "Unit must be millicores or cores.": "Unit must be millicores or cores.", + "Update period": "Update period", + "Update strategy": "Update strategy", + "Updating": "Updating", + "Upload file ({{fileTypes}}) to project": "Upload file ({{fileTypes}}) to project", + "View all ({{jobCount}})": "View all ({{jobCount}})", + "View all {{buildsLength}}": "View all {{buildsLength}}", + "View all {{size}}": "View all {{size}}", + "View all developer catalog items ({{itemCount, number}})": "View all developer catalog items ({{itemCount, number}})", + "View all quick starts ({{itemCount, number}})": "View all quick starts ({{itemCount, number}})", + "View all samples ({{itemCount, number}})": "View all samples ({{itemCount, number}})", + "View details in side panel": "View details in side panel", + "View events": "View events", + "View logs": "View logs", + "View Logs": "View Logs", + "View shortcuts": "View shortcuts", + "Visual connector": "Visual connector", + "Warning: the application grouping already exists.": "Warning: the application grouping already exists.", + "We noticed that it is taking a long time to visualize your application Topology. You can use Search to find specific resources or click Continue to keep waiting.": "We noticed that it is taking a long time to visualize your application Topology. You can use Search to find specific resources or click Continue to keep waiting.", + "You don't have access to this section due to cluster policy.": "You don't have access to this section due to cluster policy.", + "Zoom in": "Zoom in", + "Zoom out": "Zoom out" +} diff --git a/locales/en/public.json b/locales/en/public.json new file mode 100644 index 0000000..b7429e7 --- /dev/null +++ b/locales/en/public.json @@ -0,0 +1,25 @@ +{ + "(default)": "(default)", + "{{ metadataName }} is paused": "{{ metadataName }} is paused", + "Current desired pod count": "Current desired pod count", + "Error": "Error", + "Execute a smooth roll out of the new revision, based on the settings below": "Execute a smooth roll out of the new revision, based on the settings below", + "greater than pod_one": "greater than pod", + "greater than pod_other": "greater than pod", + "How should the pods be replaced when a new revision is created?": "How should the pods be replaced when a new revision is created?", + "Max surge": "Max surge", + "Max unavailable": "Max unavailable", + "of pod_one": "of pod", + "of pod_other": "of pod", + "OK": "OK", + "Percentage of total number of pods or the maximum number of pods that can be scheduled above the original number of pods(optional)": "Percentage of total number of pods or the maximum number of pods that can be scheduled above the original number of pods(optional)", + "Percentage of total number of pods or the maximum number of pods that can be unavailable during the update(optional)": "Percentage of total number of pods or the maximum number of pods that can be unavailable during the update(optional)", + "Please <2>try again.": "Please <2>try again.", + "Recreate": "Recreate", + "Resume rollouts": "Resume rollouts", + "Resume updates": "Resume updates", + "RollingUpdate": "RollingUpdate", + "Shut down all existing pods before creating new ones": "Shut down all existing pods before creating new ones", + "This will stop any new rollouts or triggers from running until resumed.": "This will stop any new rollouts or triggers from running until resumed.", + "View logs": "View logs" +} diff --git a/oc-manifest.yaml b/oc-manifest.yaml new file mode 100644 index 0000000..e0bd2c5 --- /dev/null +++ b/oc-manifest.yaml @@ -0,0 +1,111 @@ +apiVersion: template.openshift.io/v1 +kind: Template +metadata: + name: topology-console-plugin-template + annotations: + openshift.io/display-name: OpenShift Console Topology Plugin Template + openshift.io/documentation-url: "https://github.com/kubevirt-ui/topology-plugin" + iconClass: icon-nginx + tags: openshift,console,plugin,nginx,virtualization,kubevirt,topology +parameters: + - description: Name of your plugin. This name must match the name in the consolePlugin declaration in package.json. + name: PLUGIN_NAME + value: topology-console-plugin + required: true + - description: Namespace for your plugin. The namespace will be created by the template. + name: NAMESPACE + value: topology-console-plugin + required: true + - description: Container image of the plugin. + name: IMAGE + value: quay.io/repository/kubevirt-ui/topology-plugin:latest + required: true +message: >- + To enable the plugin on the cluster, run the following command: + oc patch consoles.operator.openshift.io cluster --patch '{ "spec": { "plugins": ["${PLUGIN_NAME}"] } }' --type=merge + For more information about using this template, see https://github.com/edge-infrastructure/topology-console-plugin +objects: + - apiVersion: apps/v1 + kind: Deployment + metadata: + name: "${PLUGIN_NAME}" + namespace: "${NAMESPACE}" + labels: + app: "${PLUGIN_NAME}" + app.kubernetes.io/component: "${PLUGIN_NAME}" + app.kubernetes.io/instance: "${PLUGIN_NAME}" + app.kubernetes.io/part-of: "${PLUGIN_NAME}" + app.openshift.io/runtime-namespace: "${NAMESPACE}" + spec: + replicas: 1 + selector: + matchLabels: + app: "${PLUGIN_NAME}" + template: + metadata: + labels: + app: "${PLUGIN_NAME}" + spec: + containers: + - name: "${PLUGIN_NAME}" + image: "${IMAGE}" + ports: + - containerPort: 9443 + protocol: TCP + imagePullPolicy: Always + volumeMounts: + - name: plugin-serving-cert + readOnly: true + mountPath: /var/serving-cert + volumes: + - name: plugin-serving-cert + secret: + secretName: plugin-serving-cert + defaultMode: 420 + - name: nginx-conf + configMap: + name: nginx-conf + defaultMode: 420 + restartPolicy: Always + dnsPolicy: ClusterFirst + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 25% + maxSurge: 25% + - apiVersion: v1 + kind: Service + metadata: + annotations: + service.alpha.openshift.io/serving-cert-secret-name: plugin-serving-cert + name: "${PLUGIN_NAME}" + namespace: "${NAMESPACE}" + labels: + app: "${PLUGIN_NAME}" + app.kubernetes.io/component: "${PLUGIN_NAME}" + app.kubernetes.io/instance: "${PLUGIN_NAME}" + app.kubernetes.io/part-of: "${PLUGIN_NAME}" + spec: + ports: + - name: 9443-tcp + protocol: TCP + port: 9443 + targetPort: 9443 + selector: + app: "${PLUGIN_NAME}" + type: ClusterIP + sessionAffinity: None + - apiVersion: console.openshift.io/v1alpha1 + kind: ConsolePlugin + metadata: + name: "${PLUGIN_NAME}" + annotations: + console.openshift.io/use-i18n: "true" + spec: + displayName: "Console Plugin Topology Template" + proxy: + service: + name: "${PLUGIN_NAME}" + namespace: "${NAMESPACE}" + port: 9443 + basePath: "/" diff --git a/package.json b/package.json new file mode 100644 index 0000000..f57bfc0 --- /dev/null +++ b/package.json @@ -0,0 +1,163 @@ +{ + "name": "topology-plugin", + "version": "0.0.0-fixed", + "description": "OpenShift Topology Views", + "main": "src/index.ts", + "private": true, + "scripts": { + "clean": "rm -rf dist", + "build": "NODE_ENV=production NODE_OPTIONS=--max-old-space-size=8192 rm -rf dist && yarn ts-node node_modules/.bin/webpack", + "dev": "NODE_ENV=development NODE_OPTIONS=--max-old-space-size=8192 rm -rf dist && yarn ts-node ./node_modules/.bin/webpack serve --progress", + "lint": "eslint src cypress --color", + "lint:fix": "yarn lint --fix", + "check-types": "tsc", + "ts-node": "ts-node -O '{\"module\":\"commonjs\"}'", + "test": "jest", + "test-cov": "jest --coverage", + "start-console": "./scripts/start-console.sh", + "i18n": "i18next \"src/**/*.{js,jsx,ts,tsx}\" [-oc] -c i18next-parser.config.mjs" + }, + "consolePlugin": { + "name": "topology-plugin", + "version": "0.0.0", + "displayName": "Topology Plugin", + "exposedModules": { + "reduxReducer": "src/utils/reducer.ts", + "workload": "src/components/workload", + "actions": "src/actions/provider.ts", + "exportAppContext": "src/components/export-app/export-app-context.ts", + "applicationSidebar": "src/components/application-panel", + "vcSidebar": "src/components/visual-connector" + }, + "dependencies": { + "@console/pluginAPI": "*" + } + }, + "dependencies": { + "@kubevirt-ui/components": "^0.0.10", + "@novnc/novnc": "1.3.0", + "@patternfly/quickstarts": "2.4.0", + "@patternfly/react-catalog-view-extension": "^4.96.0", + "@patternfly/react-charts": "^6.94.19", + "@patternfly/react-core": "^4.276.8", + "@patternfly/react-icons": "^4.82.8", + "@patternfly/react-table": "^4.113.0", + "@patternfly/react-topology": "^4.91.40", + "@types/byte-size": "^8.1.0", + "axios": "^0.27.2", + "byte-size": "^8.1.0", + "classnames": "^2.3.1", + "date-fns": "^2.28.0", + "enzyme": "^3.11.0", + "fast-xml-parser": "^4.0.7", + "file-saver": "^2.0.5", + "formik": "^2.2.9", + "fuzzysearch": "^1.0.3", + "git-url-parse": "^13.1.0", + "global": "^4.4.0", + "immer": "^9.0.12", + "js-abbreviation-number": "^1.4.0", + "js-yaml": "^4.1.0", + "lodash.capitalize": "^4.2.1", + "lodash.clonedeep": "^4.5.0", + "lodash.clonedeepwith": "^4.5.0", + "lodash.find": "^4.6.0", + "lodash.findkey": "^4.6.0", + "lodash.get": "^4.4.2", + "lodash.intersection": "^4.4.0", + "lodash.matches": "^4.6.0", + "lodash.memoize": "^4.1.2", + "lodash.merge": "^4.6.2", + "lodash.omit": "^4.5.0", + "lodash.orderby": "^4.6.0", + "lodash.partition": "^4.6.0", + "lodash.pick": "^4.4.0", + "lodash.pickby": "^4.6.0", + "lodash.shuffle": "^4.2.0", + "lodash.sortby": "^4.7.0", + "lodash.startcase": "^4.4.0", + "lodash.tointeger": "^4.0.4", + "lodash.topath": "^4.5.2", + "lodash.truncate": "^4.4.2", + "lodash.unionwith": "^4.6.0", + "lodash.update": "^4.10.2", + "mochawesome-report-generator": "^6.0.1", + "murmurhash-js": "^1.0.0", + "randexp": "^0.5.3", + "react": "17.0.2", + "react-copy-to-clipboard": "^5.1.0", + "react-dnd": "^9.4.0", + "react-dnd-html5-backend": "^9.4.0", + "react-dom": "17.0.2", + "react-helmet": "^6.1.0", + "react-hook-form": "^7.31.2", + "react-i18next": "^11.14.3", + "react-linkify": "^1.0.0-alpha", + "react-router-dom": "5.x", + "react-tagsinput": "^3.20.1", + "yup": "^0.27.0" + }, + "devDependencies": { + "@cypress/webpack-preprocessor": "^5.11.0", + "@kubevirt-ui/kubevirt-api": "^0.0.51", + "@openshift-console/dynamic-plugin-sdk": "0.0.18", + "@openshift-console/dynamic-plugin-sdk-internal": "0.0.11", + "@openshift-console/dynamic-plugin-sdk-webpack": "0.0.9", + "@openshift-console/plugin-shared": "^0.0.1", + "@testing-library/jest-dom": "^5.16.1", + "@testing-library/react": "^12.1.2", + "@testing-library/react-hooks": "^7.0.2", + "@testing-library/user-event": "^13.5.0", + "@types/jest": "^27.0.3", + "@types/js-yaml": "^4.0.5", + "@types/node": "^16.11.12", + "@types/react": "^17.0.40", + "@types/react-router-dom": "5.x", + "@typescript-eslint/eslint-plugin": "^5.14.0", + "@typescript-eslint/parser": "^5.14.0", + "copy-webpack-plugin": "^11.0.0", + "css-loader": "6.6.0", + "cypress": "10.3.x", + "cypress-multi-reporters": "^1.5.0", + "dnd-core": "^9.4.0", + "eslint": "^8.10.0", + "eslint-config-prettier": "^8.5.0", + "eslint-import-resolver-typescript": "^2.5.0", + "eslint-plugin-chai-friendly": "^0.7.2", + "eslint-plugin-cypress": "^2.12.1", + "eslint-plugin-import": "^2.25.4", + "eslint-plugin-jsdoc": "^37.9.7", + "eslint-plugin-prettier": "^4.0.0", + "eslint-plugin-react": "^7.29.3", + "eslint-plugin-react-hooks": "^4.3.0", + "eslint-plugin-simple-import-sort": "^7.0.0", + "fork-ts-checker-webpack-plugin": "^6.5.0", + "i18next-conv": "^12.1.1", + "i18next-parser": "^6.5.0", + "jest": "^27.4.5", + "mini-svg-data-uri": "^1.4.4", + "mocha": "^10.1.0", + "mocha-junit-reporter": "^2.0.2", + "mochawesome": "^7.0.1", + "mochawesome-merge": "^4.2.1", + "pluralize": "^8.0.0", + "prettier": "^2.5.1", + "resolve-url-loader": "^5.0.0", + "sass": "^1.49.7", + "sass-loader": "^12.4.0", + "style-loader": "^3.2.1", + "ts-jest": "^27.1.1", + "ts-loader": "9.x", + "ts-node": "10.x", + "tsconfig-paths-webpack-plugin": "^3.5.1", + "typescript": "^4.5.5", + "webpack": "^5.68.0", + "webpack-cli": "4.10.x", + "webpack-dev-server": "^4.7.4", + "yarn": "^1.22.18" + }, + "resolutions": { + "@types/react": "17.0.14", + "@types/react-dom": "17.0.14" + } +} diff --git a/scripts/start-console.sh b/scripts/start-console.sh new file mode 100755 index 0000000..316e7b7 --- /dev/null +++ b/scripts/start-console.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +set -euo pipefail + +npm_package_consolePlugin_name="kubevirt-plugin" + +CONSOLE_IMAGE=${CONSOLE_IMAGE:="quay.io/openshift/origin-console:latest"} +CONSOLE_PORT=${CONSOLE_PORT:=9000} + +echo "Starting local OpenShift console..." + +BRIDGE_USER_AUTH="disabled" +BRIDGE_K8S_MODE="off-cluster" +BRIDGE_K8S_AUTH="bearer-token" +BRIDGE_K8S_MODE_OFF_CLUSTER_SKIP_VERIFY_TLS=true +BRIDGE_K8S_MODE_OFF_CLUSTER_ENDPOINT=$(oc whoami --show-server) +BRIDGE_K8S_MODE_OFF_CLUSTER_THANOS=$(oc -n openshift-config-managed get configmap monitoring-shared-config -o jsonpath='{.data.thanosPublicURL}') +BRIDGE_K8S_MODE_OFF_CLUSTER_ALERTMANAGER=$(oc -n openshift-config-managed get configmap monitoring-shared-config -o jsonpath='{.data.alertmanagerPublicURL}') +BRIDGE_K8S_AUTH_BEARER_TOKEN=$(oc whoami --show-token 2>/dev/null) +BRIDGE_USER_SETTINGS_LOCATION="localstorage" +BRIDGE_I18N_NAMESPACES="plugin__kubevirt-plugin" + +BRIDGE_PLUGINS="${npm_package_consolePlugin_name}=http://host.docker.internal:9001" + +echo "API Server: $BRIDGE_K8S_MODE_OFF_CLUSTER_ENDPOINT" +echo "Console Image: $CONSOLE_IMAGE" +echo "Console URL: http://localhost:${CONSOLE_PORT}" + +# Prefer podman if installed. Otherwise, fall back to docker. +if [ -x "$(command -v podman)" ]; then + if [ "$(uname -s)" = "Linux" ]; then + # Use host networking on Linux since host.containers.internal is unreachable in some environments. + BRIDGE_PLUGINS="${npm_package_consolePlugin_name}=http://localhost:9001" + podman run --pull=always --rm --network=host --env-file <(set | grep BRIDGE) $CONSOLE_IMAGE + else + BRIDGE_PLUGINS="${npm_package_consolePlugin_name}=http://host.containers.internal:9001" + podman run --pull=always --rm -p "$CONSOLE_PORT":9000 --env-file <(set | grep BRIDGE) $CONSOLE_IMAGE + fi +else + BRIDGE_PLUGINS="${npm_package_consolePlugin_name}=http://host.docker.internal:9001" + docker run --pull=always --rm -p "$CONSOLE_PORT":9000 --env-file <(set | grep BRIDGE) $CONSOLE_IMAGE +fi diff --git a/src/__tests__/CloseButton/CloseButton.scss b/src/__tests__/CloseButton/CloseButton.scss new file mode 100644 index 0000000..ed197e6 --- /dev/null +++ b/src/__tests__/CloseButton/CloseButton.scss @@ -0,0 +1,14 @@ +.co-close-button { + + &--float-right { + float: right; + } + + &--no-padding { + padding: 0 !important; + } +} + +.co-sidebar-dismiss__close-button { + font-size: 16px !important; +} diff --git a/src/__tests__/CloseButton/CloseButton.tsx b/src/__tests__/CloseButton/CloseButton.tsx new file mode 100644 index 0000000..d5d39a8 --- /dev/null +++ b/src/__tests__/CloseButton/CloseButton.tsx @@ -0,0 +1,37 @@ +import React, { FC } from 'react'; +import classNames from 'classnames'; + +import { Button } from '@patternfly/react-core'; +import { CloseIcon } from '@patternfly/react-icons'; +import { useTopologyTranslation } from '@topology-utils/hooks/useTopologyTranslation'; + +import './CloseButton.scss'; + +type CloseButtonProps = { + additionalClassName?: string; + ariaLabel?: string; + dataTestID?: string; + onClick: (e: any) => void; +}; + +const CloseButton: FC = ({ + additionalClassName, + ariaLabel, + dataTestID, + onClick, +}) => { + const { t } = useTopologyTranslation(); + return ( + + ); +}; + +export default CloseButton; diff --git a/src/__tests__/DataModelProvider.spec.tsx b/src/__tests__/DataModelProvider.spec.tsx new file mode 100644 index 0000000..05ecec7 --- /dev/null +++ b/src/__tests__/DataModelProvider.spec.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import { mount, ReactWrapper } from 'enzyme'; + +import { useURLPoll } from '@openshift-console/dynamic-plugin-sdk-internal'; + +import TopologyDataRenderer from '../components/page/TopologyDataRenderer'; +import DataModelProvider from '../data-transforms/DataModelProvider'; +import TopologyDataRetriever from '../data-transforms/TopologyDataRetriever'; +import { TopologyViewType } from '../utils/types/topology-types'; + +jest.mock('@console/plugin-sdk/src/api/useExtensions', () => ({ + useExtensions: () => [], +})); +jest.mock('@console/shared', () => { + const ActualShared = jest.requireActual('@console/shared'); + return { + ...ActualShared, + useQueryParams: () => new Map(), + }; +}); + +type Props = { + className?: string; +}; + +describe('DataModelProvider', () => { + let wrapper: ReactWrapper; + const spyUseURLPoll = jest.fn(useURLPoll); + + beforeEach(() => { + spyUseURLPoll.mockReturnValue([{}, null, false]); + wrapper = mount( + + + , + { + wrappingComponent: ({ children }) => {children}, + }, + ); + }); + + it('should render inner components', () => { + expect(wrapper.find(TopologyDataRetriever)).toHaveLength(1); + expect(wrapper.find(TopologyDataRenderer)).toHaveLength(1); + }); +}); diff --git a/src/__tests__/Graph.spec.tsx b/src/__tests__/Graph.spec.tsx new file mode 100644 index 0000000..88381b0 --- /dev/null +++ b/src/__tests__/Graph.spec.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import { mount } from 'enzyme'; + +import { ConnectedTopologyView } from '../components/page/TopologyView'; +import { baseDataModelGetter } from '../data-transforms/data-transformer'; +import { TopologyViewType } from '../utils/types/topology-types'; + +import { MockGraphResources } from './graph-test-data'; + +describe('Graph', () => { + let topologyData; + let graphWrapper; + let mockSelectFn; + + beforeEach(() => { + mockSelectFn = jest.fn(); + const model = { nodes: [], edges: [] }; + topologyData = baseDataModelGetter(model, 'test-project', MockGraphResources, []); + graphWrapper = mount( + {}} // eslint-disable-line @typescript-eslint/no-empty-function + onSupportedFiltersChange={() => {}} // eslint-disable-line @typescript-eslint/no-empty-function + onSupportedKindsChange={() => {}} // eslint-disable-line @typescript-eslint/no-empty-function + />, + ); + }); + + xit('should display the workload nodes', () => { + expect(graphWrapper.find('.odc-base-node').length).toBe(7); + }); + + xit('should display the connectors', () => { + expect(graphWrapper.find('.odc-base-edge').length).toBe(3); + }); + + xit('should display the groups', () => { + expect(graphWrapper.find('.odc-default-group').length).toBe(3); + }); + + xit('should call onSelect on a node click', () => { + const node = graphWrapper.find('.odc-base-node').first(); + const nodeEventHandler = node.find('[data-test-id="base-node-handler"]').first(); + nodeEventHandler.simulate('click'); + expect(mockSelectFn).toHaveBeenCalled(); + }); + + xit('should display the create connector component on node hover', () => { + expect(graphWrapper.find('.odc-dragging-create-connector').exists()).toBeFalsy(); + const node = graphWrapper.find('.odc-base-node').first(); + const nodeEventHandler = node.find('[data-test-id="base-node-handler"]').first(); + nodeEventHandler.simulate('mouseenter'); + expect(graphWrapper.find('.odc-dragging-create-connector').exists()).toBeTruthy(); + nodeEventHandler.simulate('mouseleave'); + expect(graphWrapper.find('.odc-dragging-create-connector').exists()).toBeFalsy(); + }); + + xit('should highlight a connector on hover', () => { + expect(graphWrapper.find('.odc-base-edge.is-hover').exists()).toBeFalsy(); + const connectorHandler = graphWrapper.find('[data-test-id="connects-to-handler"]').first(); + connectorHandler.simulate('mouseenter'); + expect(graphWrapper.find('.odc-base-edge.is-hover').exists()).toBeTruthy(); + connectorHandler.simulate('mouseleave'); + expect(connectorHandler.find('.odc-base-edge.is-hover').exists()).toBeFalsy(); + }); + + xit('should display the remove icon on a connector on hover', () => { + const connectorHandler = graphWrapper.find('[data-test-id="connects-to-handler"]').first(); + expect(graphWrapper.find('.odc-base-edge.is-hover').exists()).toBeFalsy(); + connectorHandler.simulate('mouseenter'); + expect(graphWrapper.find('.odc-connects-to__remove-bg').exists()).toBeTruthy(); + connectorHandler.simulate('mouseleave'); + expect(graphWrapper.find('.odc-base-edge.is-hover').exists()).toBeFalsy(); + }); +}); diff --git a/src/__tests__/TopologyPage.spec.tsx b/src/__tests__/TopologyPage.spec.tsx new file mode 100644 index 0000000..e7ad6bb --- /dev/null +++ b/src/__tests__/TopologyPage.spec.tsx @@ -0,0 +1,158 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { useK8sWatchResources } from '@openshift-console/dynamic-plugin-sdk'; + +import { TopologyPage } from '../components/page/TopologyPage'; +import { usePreferredTopologyView } from '../user-preferences/usePreferredTopologyView'; +import useQueryParams from '../utils/hooks/useQueryParams'; +import { TopologyViewType } from '../utils/types/topology-types'; + +jest.mock('@console/internal/components/hooks/k8s-watch-hook', () => ({ + useK8sWatchResources: jest.fn(), +})); + +jest.mock('react', () => { + const ActualReact = jest.requireActual('react'); + return { + ...ActualReact, + useContext: () => jest.fn(), + }; +}); + +jest.mock('react-redux', () => { + const ActualReactRedux = jest.requireActual('react-redux'); + return { + ...ActualReactRedux, + useSelector: jest.fn(), + useDispatch: jest.fn(), + }; +}); + +let mockViewParam = ''; + +jest.mock('@console/shared', () => { + const ActualShared = jest.requireActual('@console/shared'); + return { + ...ActualShared, + useQueryParams: jest.fn(), + useUserSettingsCompatibility: jest.fn(), + }; +}); + +jest.mock('../user-preferences/usePreferredTopologyView', () => ({ + usePreferredTopologyView: jest.fn(), +})); + +const match = { params: { name: 'default' }, isExact: true, path: '', url: '' }; + +describe('Topology page tests', () => { + beforeEach(() => { + mockViewParam = ''; + (useK8sWatchResources as jest.Mock).mockReturnValue({ + projects: { data: [], loaded: true, loadError: '' }, + }); + (useQueryParams as jest.Mock).mockReturnValue(new Map().set('view', mockViewParam)); + }); + + it('should render topology page', () => { + (useUserSettingsCompatibility as jest.Mock).mockReturnValue(['', () => {}]); // eslint-disable-line @typescript-eslint/no-empty-function + (usePreferredTopologyView as jest.Mock).mockReturnValue(['', true]); + const wrapper = shallow(); + expect(wrapper.find(NamespacedPage).exists()).toBe(true); + }); + + it('should default to graph view', () => { + (useUserSettingsCompatibility as jest.Mock).mockReturnValue(['', () => {}, true]); // eslint-disable-line @typescript-eslint/no-empty-function + (usePreferredTopologyView as jest.Mock).mockReturnValue(['', true]); + const wrapper = shallow(); + expect(wrapper.find('[data-test-id="topology-list-page"]').exists()).toBe(false); + }); + + it('should allow setting default to list view', () => { + (useUserSettingsCompatibility as jest.Mock).mockReturnValue(['', () => {}]); // eslint-disable-line @typescript-eslint/no-empty-function + (usePreferredTopologyView as jest.Mock).mockReturnValue(['', true]); + const wrapper = shallow( + , + ); + expect(wrapper.find('[data-test-id="topology-list-page"]').exists()).toBe(true); + }); + + it('should render view from URL view path and ignore userSettings if it is available', () => { + (useUserSettingsCompatibility as jest.Mock).mockReturnValue(['list', () => {}, true]); // eslint-disable-line @typescript-eslint/no-empty-function + (usePreferredTopologyView as jest.Mock).mockReturnValue(['list', true]); + (useQueryParams as jest.Mock).mockReturnValue(new Map().set('view', 'graph')); + const viewMatch = { + params: { name: 'default', view: 'graph' }, + isExact: true, + path: '/topology', + url: '', + }; + const wrapper = shallow(); + expect(wrapper.find('[data-test-id="topology-list-page"]').exists()).toBe(false); + }); + + it('should render view using the preferred view from user settings if it exists and does not have value "latest", and all user settings have loaded', () => { + (useUserSettingsCompatibility as jest.Mock).mockReturnValue(['list', () => {}, true]); // eslint-disable-line @typescript-eslint/no-empty-function + (usePreferredTopologyView as jest.Mock).mockReturnValue(['graph', true]); + const wrapper = shallow( + , + ); + expect(wrapper.find('[data-test-id="topology-list-page"]').exists()).toBe(false); + }); + + it('should render view using the last viewed from user settings if it exists and preferred view has value "latest", and all user settings have loaded', () => { + (useUserSettingsCompatibility as jest.Mock).mockReturnValue(['graph', () => {}, true]); // eslint-disable-line @typescript-eslint/no-empty-function + (usePreferredTopologyView as jest.Mock).mockReturnValue(['latest', true]); + const wrapper = shallow( + , + ); + expect(wrapper.find('[data-test-id="topology-list-page"]').exists()).toBe(false); + }); + + it('should render view using the last viewed from user settings if it exists and preferred view does not exist", and all user settings have loaded', () => { + (useUserSettingsCompatibility as jest.Mock).mockReturnValue(['list', () => {}, true]); // eslint-disable-line @typescript-eslint/no-empty-function + (usePreferredTopologyView as jest.Mock).mockReturnValue([undefined, true]); + const wrapper = shallow( + , + ); + expect(wrapper.find('[data-test-id="topology-list-page"]').exists()).toBe(true); + }); + + it('should render view using the default view if preferred and last view from user settings does not exist", and all user settings have loaded', () => { + (useUserSettingsCompatibility as jest.Mock).mockReturnValue([undefined, () => {}, true]); // eslint-disable-line @typescript-eslint/no-empty-function + (usePreferredTopologyView as jest.Mock).mockReturnValue([undefined, true]); + const wrapper = shallow( + , + ); + expect(wrapper.find('[data-test-id="topology-list-page"]').exists()).toBe(true); + }); + + it('should continue to support URL view path for graph', () => { + (useUserSettingsCompatibility as jest.Mock).mockReturnValue(['', () => {}, true]); // eslint-disable-line @typescript-eslint/no-empty-function + (useUserSettingsCompatibility as jest.Mock).mockReturnValue(['', () => {}]); // eslint-disable-line @typescript-eslint/no-empty-function + (useQueryParams as jest.Mock).mockReturnValue(new Map().set('view', 'graph')); + const viewMatch = { + params: { name: 'default' }, + isExact: true, + path: '/topology', + url: '', + }; + const wrapper = shallow(); + expect(wrapper.find('[data-test-id="topology-list-page"]').exists()).toBe(false); + }); + + it('should continue to support URL view path for list', () => { + (useUserSettingsCompatibility as jest.Mock).mockReturnValue(['', () => {}]); // eslint-disable-line @typescript-eslint/no-empty-function + (usePreferredTopologyView as jest.Mock).mockReturnValue(['', true]); + (useQueryParams as jest.Mock).mockReturnValue(new Map().set('view', 'list')); + const viewMatch = { + params: { name: 'default' }, + isExact: true, + path: '/topology', + url: '', + }; + const wrapper = shallow(); + expect(wrapper.find('[data-test-id="topology-list-page"]').exists()).toBe(true); + }); +}); diff --git a/src/__tests__/TopologyPageToolbar.spec.tsx b/src/__tests__/TopologyPageToolbar.spec.tsx new file mode 100644 index 0000000..a47f242 --- /dev/null +++ b/src/__tests__/TopologyPageToolbar.spec.tsx @@ -0,0 +1,119 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { useAccessReview } from '@openshift-console/dynamic-plugin-sdk'; +import { Button, Tooltip } from '@patternfly/react-core'; + +import TopologyPageToolbar from '../components/page/TopologyPageToolbar'; +import { TopologyViewType } from '../utils/types/topology-types'; + +jest.mock('react', () => { + const ActualReact = jest.requireActual('react'); + return { + ...ActualReact, + useContext: () => jest.fn(), + }; +}); + +jest.mock('react-redux', () => { + const ActualReactRedux = jest.requireActual('react-redux'); + return { + ...ActualReactRedux, + useSelector: jest.fn(), + useDispatch: jest.fn(), + }; +}); + +jest.mock('@console/shared', () => { + const ActualShared = jest.requireActual('@console/shared'); + return { + ...ActualShared, + useQueryParams: () => new Map(), + }; +}); + +describe('TopologyPageToolbar tests', () => { + let spyUseAccessReview; + + beforeEach(() => { + spyUseAccessReview = jest.fn(useAccessReview); + spyUseAccessReview.mockReturnValue(true); + }); + + afterEach(() => { + spyUseAccessReview.mockReset(); + }); + + it('should render view shortcuts button on topology page toolbar', () => { + const mockViewChange = jest.fn(); + spyOn(React, 'useContext').and.returnValue({ + isEmptyModel: false, + namespace: 'test-namespace', + }); + const wrapper = shallow( + , + ); + expect(wrapper.find('[data-test-id="topology-view-shortcuts"]').exists()).toBe(true); + }); + + it('should render view shortcuts button on topology list page toolbar', () => { + const mockViewChange = jest.fn(); + spyOn(React, 'useContext').and.returnValue({ + isEmptyModel: false, + namespace: 'test-namespace', + }); + const wrapper = shallow( + , + ); + expect(wrapper.find('[data-test-id="topology-view-shortcuts"]').exists()).toBe(true); + }); + + it('should show the topology icon when on topology list page', () => { + const mockViewChange = jest.fn(); + spyOn(React, 'useContext').and.returnValue({ + isEmptyModel: false, + namespace: 'test-namespace', + }); + const wrapper = shallow( + , + ); + expect(wrapper.find(Tooltip).props().content).toBe('Graph view'); + }); + + it('should show the topology list icon when on topology page', () => { + const mockViewChange = jest.fn(); + spyOn(React, 'useContext').and.returnValue({ + isEmptyModel: false, + namespace: 'test-namespace', + }); + const wrapper = shallow( + , + ); + expect(wrapper.find(Tooltip).props().content).toBe('List view'); + }); + + it('should not contain view switcher when when no project is selected', () => { + const mockViewChange = jest.fn(); + spyOn(React, 'useContext').and.returnValue({ + isEmptyModel: false, + namespace: undefined, + }); + const wrapper = shallow( + , + ); + expect(wrapper.find(Button).exists()).toBe(false); + }); + + it('should disable view switcher when no model', () => { + const mockViewChange = jest.fn(); + spyOn(React, 'useContext').and.returnValue({ + isEmptyModel: true, + namespace: 'test-namespace', + }); + const wrapper = shallow( + , + ); + const switcher = wrapper.find(Button); + expect(switcher.at(1).props().isDisabled).toBe(true); + }); +}); diff --git a/src/__tests__/TopologyShortcuts.spec.tsx b/src/__tests__/TopologyShortcuts.spec.tsx new file mode 100644 index 0000000..0383e7b --- /dev/null +++ b/src/__tests__/TopologyShortcuts.spec.tsx @@ -0,0 +1,117 @@ +import { mount, shallow } from 'enzyme'; + +import Shortcut from '../components/graph-view/components/Shortcut/Shortcut'; +import { getTopologyShortcuts } from '../components/graph-view/TopologyShortcuts'; +import { TopologyViewType } from '../utils/types/topology-types'; + +describe('TopologyShortcuts tests', () => { + it('should show reduced list in view shortcuts popover on topology toolbar when there are no workloads', () => { + const wrapper = mount( + getTopologyShortcuts(jest.fn(), { + supportedFileTypes: undefined, + isEmptyModel: true, + viewType: TopologyViewType.graph, + allImportAccess: true, + }), + ); + + expect(wrapper.find(Shortcut).exists()).toBe(true); + expect(wrapper.find('[data-test-id="open-quick-search"]').exists()).toBe(true); + expect(wrapper.find('[data-test-id="ctrl-button"]').exists()).toBe(true); + expect(wrapper.find('[data-test-id="Spacebar-button"]').exists()).toBe(true); + expect(wrapper.find('[data-test-id="create-connector-handle"]').exists()).toBe(false); + expect(wrapper.find('[data-test-id="hover"]').exists()).toBe(false); + expect(wrapper.find('[data-test-id="context-menu"]').exists()).toBe(false); + expect(wrapper.find('[data-test-id="right-click"]').exists()).toBe(false); + expect(wrapper.find('[data-test-id="view-details"]').exists()).toBe(false); + expect(wrapper.find('[data-test-id="click"]').exists()).toBe(false); + expect(wrapper.find('[data-test-id="edit-application-grouping"]').exists()).toBe(false); + wrapper.unmount(); + }); + + it('should show dragNdrop action in the view shortcuts popover on topology toolbar only when supportedFileTypes is not empty', () => { + const wrapper = mount( + getTopologyShortcuts(jest.fn(), { + supportedFileTypes: ['jar'], + isEmptyModel: true, + viewType: TopologyViewType.graph, + allImportAccess: true, + }), + ); + + expect(wrapper.find(Shortcut).exists()).toBe(true); + expect(wrapper.find('[data-test-id="upload-file"]').exists()).toBe(true); + expect(wrapper.find('[data-test-id="drag-and-drop"]').exists()).toBe(true); + wrapper.unmount(); + }); + + it('should show reduced list in the view shortcuts popover on topology toolbar for list view', () => { + const wrapper = shallow( + getTopologyShortcuts(jest.fn(), { + supportedFileTypes: ['jar'], + isEmptyModel: false, + viewType: TopologyViewType.list, + allImportAccess: true, + }), + ); + + expect(wrapper.find('[data-test-id="upload-file"]').exists()).toBe(true); + expect(wrapper.find('[data-test-id="view-details"]').exists()).toBe(true); + expect(wrapper.find('[data-test-id="open-quick-search"]').exists()).toBe(true); + expect(wrapper.find('[data-test-id="create-connector-handle"]').exists()).toBe(false); + expect(wrapper.find('[data-test-id="context-menu"]').exists()).toBe(false); + expect(wrapper.find('[data-test-id="edit-application-grouping"]').exists()).toBe(false); + }); + it('should show the full list in the view shortcuts popover on topology toolbar for graph view', () => { + const wrapper = shallow( + getTopologyShortcuts(jest.fn(), { + supportedFileTypes: ['jar'], + isEmptyModel: false, + viewType: TopologyViewType.graph, + allImportAccess: true, + }), + ); + expect(wrapper.find('[data-test-id="move"]').exists()).toBe(true); + expect(wrapper.find('[data-test-id="upload-file"]').exists()).toBe(true); + expect(wrapper.find('[data-test-id="context-menu"]').exists()).toBe(true); + expect(wrapper.find('[data-test-id="create-connector-handle"]').exists()).toBe(true); + expect(wrapper.find('[data-test-id="view-details"]').exists()).toBe(true); + expect(wrapper.find('[data-test-id="open-quick-search"]').exists()).toBe(true); + expect(wrapper.find('[data-test-id="edit-application-grouping"]').exists()).toBe(true); + }); + it('should show only view details and quick search actions in list view', () => { + const wrapper = shallow( + getTopologyShortcuts(jest.fn(), { + supportedFileTypes: ['jar'], + isEmptyModel: false, + viewType: TopologyViewType.list, + allImportAccess: false, + }), + ); + expect(wrapper.find('[data-test-id="move"]').exists()).toBe(false); + expect(wrapper.find('[data-test-id="view-details"]').exists()).toBe(true); + expect(wrapper.find('[data-test-id="open-quick-search"]').exists()).toBe(true); + expect(wrapper.find('[data-test-id="create-connector-handle"]').exists()).toBe(false); + expect(wrapper.find('[data-test-id="context-menu"]').exists()).toBe(false); + expect(wrapper.find('[data-test-id="edit-application-grouping"]').exists()).toBe(false); + expect(wrapper.find('[data-test-id="upload-file"]').exists()).toBe(false); + }); + + it('should show only view details and quick search actions in graph view', () => { + const wrapper = shallow( + getTopologyShortcuts(jest.fn(), { + supportedFileTypes: ['jar'], + isEmptyModel: false, + viewType: TopologyViewType.graph, + allImportAccess: false, + }), + ); + expect(wrapper.find('[data-test-id="move"]').exists()).toBe(true); + expect(wrapper.find('[data-test-id="view-details"]').exists()).toBe(true); + expect(wrapper.find('[data-test-id="open-quick-search"]').exists()).toBe(true); + expect(wrapper.find('[data-test-id="create-connector-handle"]').exists()).toBe(false); + expect(wrapper.find('[data-test-id="context-menu"]').exists()).toBe(false); + expect(wrapper.find('[data-test-id="edit-application-grouping"]').exists()).toBe(false); + expect(wrapper.find('[data-test-id="upload-file"]').exists()).toBe(false); + }); +}); diff --git a/src/__tests__/TopologySideBar.spec.tsx b/src/__tests__/TopologySideBar.spec.tsx new file mode 100644 index 0000000..819c129 --- /dev/null +++ b/src/__tests__/TopologySideBar.spec.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { useUserSettings } from '@openshift-console/dynamic-plugin-sdk-internal'; +import { Button } from '@patternfly/react-core'; + +import TopologySideBar from '../components/side-bar/TopologySideBar'; + +import CloseButton from './CloseButton/CloseButton'; + +jest.mock('@console/shared/src/hooks/useUserSettings', () => ({ + useUserSettings: jest.fn(), +})); + +const mockUserSettings = useUserSettings as jest.Mock; + +describe('TopologySideBar:', () => { + const props = { + show: true, + onClose: () => '', + }; + + it('renders a SideBar', () => { + mockUserSettings.mockReturnValue([100, () => {}, true]); + const wrapper = shallow(); + expect(wrapper.exists()).toBeTruthy(); + }); + + it('clicking on close button should call the onClose callback function', () => { + mockUserSettings.mockReturnValue([100, () => {}, true]); + const onClose = jest.fn(); + const wrapper = shallow(); + wrapper.find(CloseButton).shallow().find(Button).simulate('click'); + expect(onClose).toHaveBeenCalled(); + }); +}); diff --git a/src/__tests__/graph-test-data.ts b/src/__tests__/graph-test-data.ts new file mode 100644 index 0000000..6fc5f33 --- /dev/null +++ b/src/__tests__/graph-test-data.ts @@ -0,0 +1,2631 @@ +import { TopologyDataResources } from '@openshift-console/dynamic-plugin-sdk/lib/extensions/topology-types'; + +export const MockGraphResources: TopologyDataResources = { + deploymentConfigs: { + loaded: true, + loadError: '', + data: [ + { + kind: 'deploymentconfig', + metadata: { + annotations: { + 'app.openshift.io/connects-to': 'mysql', + description: 'Defines how to deploy the application server', + 'template.alpha.openshift.io/wait-for-ready': 'true', + }, + resourceVersion: '460792', + name: 'cakephp-mysql-example', + uid: '86ddea2d-ddf4-11e9-b72f-0a580a810024', + creationTimestamp: '2019-09-23T11:23:11Z', + generation: 1, + namespace: 'jeff-project', + labels: { + app: 'cakephp-mysql-example', + 'app.kubernetes.io/part-of': 'application-1', + template: 'cakephp-mysql-example', + 'template.openshift.io/template-instance-owner': '86b6b79a-ddf4-11e9-a662-0a580a820020', + }, + }, + // spec: { + // strategy: { + // type: 'Recreate', + // recreateParams: { + // timeoutSeconds: 600, + // pre: { + // failurePolicy: 'Retry', + // execNewPod: { + // command: ['./migrate-database.sh'], + // containerName: 'cakephp-mysql-example', + // }, + // }, + // }, + // resources: {}, + // activeDeadlineSeconds: 21600, + // }, + // triggers: [ + // { + // type: 'ImageChange', + // imageChangeParams: { + // automatic: true, + // containerNames: ['cakephp-mysql-example'], + // from: { + // kind: 'ImageStreamTag', + // namespace: 'jeff-project', + // name: 'cakephp-mysql-example:latest', + // }, + // }, + // }, + // { type: 'ConfigChange' }, + // ], + // replicas: 1, + // revisionHistoryLimit: 10, + // test: false, + // selector: { name: 'cakephp-mysql-example' }, + // template: { + // metadata: { + // name: 'cakephp-mysql-example', + // creationTimestamp: null, + // labels: { name: 'cakephp-mysql-example' }, + // }, + // spec: { + // containers: [ + // { + // resources: { limits: { memory: '512Mi' } }, + // readinessProbe: { + // httpGet: { path: '/health.php', port: 8080, scheme: 'HTTP' }, + // initialDelaySeconds: 3, + // timeoutSeconds: 3, + // periodSeconds: 60, + // successThreshold: 1, + // failureThreshold: 3, + // }, + // terminationMessagePath: '/dev/termination-log', + // name: 'cakephp-mysql-example', + // livenessProbe: { + // httpGet: { path: '/health.php', port: 8080, scheme: 'HTTP' }, + // initialDelaySeconds: 30, + // timeoutSeconds: 3, + // periodSeconds: 60, + // successThreshold: 1, + // failureThreshold: 3, + // }, + // env: [ + // { name: 'DATABASE_SERVICE_NAME', value: 'mysql' }, + // { name: 'DATABASE_ENGINE', value: 'mysql' }, + // { name: 'DATABASE_NAME', value: 'default' }, + // { + // name: 'DATABASE_USER', + // valueFrom: { + // secretKeyRef: { name: 'cakephp-mysql-example', key: 'database-user' }, + // }, + // }, + // { + // name: 'DATABASE_PASSWORD', + // valueFrom: { + // secretKeyRef: { name: 'cakephp-mysql-example', key: 'database-password' }, + // }, + // }, + // { + // name: 'CAKEPHP_SECRET_TOKEN', + // valueFrom: { + // secretKeyRef: { + // name: 'cakephp-mysql-example', + // key: 'cakephp-secret-token', + // }, + // }, + // }, + // { + // name: 'CAKEPHP_SECURITY_SALT', + // valueFrom: { + // secretKeyRef: { + // name: 'cakephp-mysql-example', + // key: 'cakephp-security-salt', + // }, + // }, + // }, + // { name: 'OPCACHE_REVALIDATE_FREQ', value: '2' }, + // ], + // ports: [{ containerPort: 8080, protocol: 'TCP' }], + // imagePullPolicy: ImagePullPolicy.IfNotPresent, + // terminationMessagePolicy: 'File', + // image: ' ', + // }, + // ], + // restartPolicy: 'Always', + // terminationGracePeriodSeconds: 30, + // dnsPolicy: 'ClusterFirst', + // securityContext: {}, + // schedulerName: 'default-scheduler', + // }, + // }, + // }, + // status: { + // latestVersion: 0, + // observedGeneration: 1, + // replicas: 0, + // updatedReplicas: 0, + // availableReplicas: 0, + // unavailableReplicas: 0, + // conditions: [ + // { + // type: 'Available', + // status: 'False', + // lastUpdateTime: '2019-09-23T11:23:11Z', + // lastTransitionTime: '2019-09-23T11:23:11Z', + // message: 'Deployment config does not have minimum availability.', + // }, + // ], + // }, + }, + { + kind: 'deploymentconfig', + metadata: { + annotations: { description: 'Defines how to deploy the application server' }, + resourceVersion: '17190', + name: 'dotnet-example', + uid: '7f4ffb69-ddf4-11e9-b72f-0a580a810024', + creationTimestamp: '2019-09-23T11:22:58Z', + generation: 1, + namespace: 'jeff-project', + labels: { + 'app.kubernetes.io/part-of': 'application-1', + 'template.openshift.io/template-instance-owner': '7f25e822-ddf4-11e9-a662-0a580a820020', + }, + }, + // spec: { + // strategy: { + // type: 'Rolling', + // rollingParams: { + // updatePeriodSeconds: 1, + // intervalSeconds: 1, + // timeoutSeconds: 600, + // maxUnavailable: '25%', + // maxSurge: '25%', + // }, + // resources: {}, + // activeDeadlineSeconds: 21600, + // }, + // triggers: [ + // { + // type: 'ImageChange', + // imageChangeParams: { + // automatic: true, + // containerNames: ['dotnet-app'], + // from: { + // kind: 'ImageStreamTag', + // namespace: 'jeff-project', + // name: 'dotnet-example:latest', + // }, + // }, + // }, + // { type: 'ConfigChange' }, + // ], + // replicas: 1, + // revisionHistoryLimit: 10, + // test: false, + // selector: { name: 'dotnet-example' }, + // template: { + // metadata: { + // name: 'dotnet-example', + // creationTimestamp: null, + // labels: { name: 'dotnet-example' }, + // }, + // spec: { + // containers: [ + // { + // resources: { limits: { memory: '128Mi' } }, + // readinessProbe: { + // httpGet: { path: '/', port: 8080, scheme: 'HTTP' }, + // initialDelaySeconds: 10, + // timeoutSeconds: 30, + // periodSeconds: 10, + // successThreshold: 1, + // failureThreshold: 3, + // }, + // terminationMessagePath: '/dev/termination-log', + // name: 'dotnet-app', + // livenessProbe: { + // httpGet: { path: '/', port: 8080, scheme: 'HTTP' }, + // initialDelaySeconds: 40, + // timeoutSeconds: 15, + // periodSeconds: 10, + // successThreshold: 1, + // failureThreshold: 3, + // }, + // ports: [{ containerPort: 8080, protocol: 'TCP' }], + // imagePullPolicy: ImagePullPolicy.IfNotPresent, + // terminationMessagePolicy: 'File', + // image: ' ', + // }, + // ], + // restartPolicy: 'Always', + // terminationGracePeriodSeconds: 30, + // dnsPolicy: 'ClusterFirst', + // securityContext: {}, + // schedulerName: 'default-scheduler', + // }, + // }, + // }, + // status: { + // latestVersion: 0, + // observedGeneration: 1, + // replicas: 0, + // updatedReplicas: 0, + // availableReplicas: 0, + // unavailableReplicas: 0, + // conditions: [ + // { + // type: 'Available', + // status: 'False', + // lastUpdateTime: '2019-09-23T11:22:59Z', + // lastTransitionTime: '2019-09-23T11:22:59Z', + // message: 'Deployment config does not have minimum availability.', + // }, + // ], + // }, + }, + { + kind: 'deploymentconfig', + metadata: { + annotations: { + 'app.openshift.io/connects-to': 'perl', + description: 'Defines how to deploy the database', + 'template.alpha.openshift.io/wait-for-ready': 'true', + }, + resourceVersion: '460861', + name: 'mysql', + uid: '86e228cb-ddf4-11e9-a662-0a580a820020', + creationTimestamp: '2019-09-23T11:23:11Z', + generation: 1, + namespace: 'jeff-project', + labels: { + app: 'cakephp-mysql-example', + 'app.kubernetes.io/part-of': 'application-2', + template: 'cakephp-mysql-example', + 'template.openshift.io/template-instance-owner': '86b6b79a-ddf4-11e9-a662-0a580a820020', + }, + }, + // spec: { + // strategy: { + // type: 'Recreate', + // recreateParams: { timeoutSeconds: 600 }, + // resources: {}, + // activeDeadlineSeconds: 21600, + // }, + // triggers: [ + // { + // type: 'ImageChange', + // imageChangeParams: { + // automatic: true, + // containerNames: ['mysql'], + // from: { kind: 'ImageStreamTag', namespace: 'openshift', name: 'mysql:5.7' }, + // }, + // }, + // { type: 'ConfigChange' }, + // ], + // replicas: 1, + // revisionHistoryLimit: 10, + // test: false, + // selector: { name: 'mysql' }, + // template: { + // metadata: { name: 'mysql', creationTimestamp: null, labels: { name: 'mysql' } }, + // spec: { + // volumes: [{ name: 'data', emptyDir: {} }], + // containers: [ + // { + // resources: { limits: { memory: '512Mi' } }, + // readinessProbe: { + // exec: { + // command: [ + // '/bin/sh', + // '-i', + // '-c', + // "MYSQL_PWD='Wrqbxggbr4mFYGQX' mysql -h 127.0.0.1 -u cakephp -D default -e 'SELECT 1'", + // ], + // }, + // initialDelaySeconds: 5, + // timeoutSeconds: 1, + // periodSeconds: 10, + // successThreshold: 1, + // failureThreshold: 3, + // }, + // terminationMessagePath: '/dev/termination-log', + // name: 'mysql', + // livenessProbe: { + // tcpSocket: { port: 3306 }, + // initialDelaySeconds: 30, + // timeoutSeconds: 1, + // periodSeconds: 10, + // successThreshold: 1, + // failureThreshold: 3, + // }, + // env: [ + // { + // name: 'MYSQL_USER', + // valueFrom: { + // secretKeyRef: { name: 'cakephp-mysql-example', key: 'database-user' }, + // }, + // }, + // { + // name: 'MYSQL_PASSWORD', + // valueFrom: { + // secretKeyRef: { name: 'cakephp-mysql-example', key: 'database-password' }, + // }, + // }, + // { name: 'MYSQL_DATABASE', value: 'default' }, + // ], + // ports: [{ containerPort: 3306, protocol: 'TCP' }], + // imagePullPolicy: ImagePullPolicy.IfNotPresent, + // volumeMounts: [{ name: 'data', mountPath: '/var/lib/mysql/data' }], + // terminationMessagePolicy: 'File', + // image: ' ', + // }, + // ], + // restartPolicy: 'Always', + // terminationGracePeriodSeconds: 30, + // dnsPolicy: 'ClusterFirst', + // securityContext: {}, + // schedulerName: 'default-scheduler', + // }, + // }, + // }, + // status: { + // latestVersion: 0, + // observedGeneration: 1, + // replicas: 0, + // updatedReplicas: 0, + // availableReplicas: 0, + // unavailableReplicas: 0, + // conditions: [ + // { + // type: 'Available', + // status: 'False', + // lastUpdateTime: '2019-09-23T11:23:11Z', + // lastTransitionTime: '2019-09-23T11:23:11Z', + // message: 'Deployment config does not have minimum availability.', + // }, + // ], + // }, + }, + { + kind: 'deploymentconfig', + metadata: { + annotations: { 'openshift.io/generated-by': 'OpenShiftWebConsole' }, + resourceVersion: '29644', + name: 'perl', + uid: 'b69703c0-ddf9-11e9-b72f-0a580a810024', + creationTimestamp: '2019-09-23T12:00:19Z', + generation: 2, + namespace: 'jeff-project', + labels: { + app: 'perl', + 'app.kubernetes.io/component': 'perl', + 'app.kubernetes.io/instance': 'perl', + }, + }, + // spec: { + // strategy: { + // type: 'Rolling', + // rollingParams: { + // updatePeriodSeconds: 1, + // intervalSeconds: 1, + // timeoutSeconds: 600, + // maxUnavailable: '25%', + // maxSurge: '25%', + // }, + // resources: {}, + // activeDeadlineSeconds: 21600, + // }, + // triggers: [ + // { + // type: 'ImageChange', + // imageChangeParams: { + // automatic: true, + // containerNames: ['perl'], + // from: { kind: 'ImageStreamTag', namespace: 'jeff-project', name: 'perl:latest' }, + // lastTriggeredImage: + // 'perl@sha256:711837fda379e492e351c0379ab697effc7e9c61dac2bef731073ac1138baad1', + // }, + // }, + // { type: 'ConfigChange' }, + // ], + // replicas: 1, + // revisionHistoryLimit: 10, + // test: false, + // selector: { app: 'perl', deploymentconfig: 'perl' }, + // template: { + // metadata: { + // creationTimestamp: null, + // labels: { app: 'perl', deploymentconfig: 'perl' }, + // annotations: { 'openshift.io/generated-by': 'OpenShiftWebConsole' }, + // }, + // spec: { + // containers: [ + // { + // name: 'perl', + // image: + // 'perl@sha256:711837fda379e492e351c0379ab697effc7e9c61dac2bef731073ac1138baad1', + // ports: [{ containerPort: 8080, protocol: 'TCP' }], + // resources: {}, + // terminationMessagePath: '/dev/termination-log', + // terminationMessagePolicy: 'File', + // imagePullPolicy: ImagePullPolicy.IfNotPresent, + // }, + // ], + // restartPolicy: 'Always', + // terminationGracePeriodSeconds: 30, + // dnsPolicy: 'ClusterFirst', + // securityContext: {}, + // schedulerName: 'default-scheduler', + // }, + // }, + // }, + // status: { + // latestVersion: 1, + // observedGeneration: 2, + // replicas: 0, + // updatedReplicas: 0, + // availableReplicas: 0, + // unavailableReplicas: 0, + // details: { message: 'config change', causes: [{ type: 'ConfigChange' }] }, + // conditions: [ + // { + // type: 'Available', + // status: 'False', + // lastUpdateTime: '2019-09-23T12:00:19Z', + // lastTransitionTime: '2019-09-23T12:00:19Z', + // message: 'Deployment config does not have minimum availability.', + // }, + // { + // type: 'Progressing', + // status: 'False', + // lastUpdateTime: '2019-09-23T12:10:32Z', + // lastTransitionTime: '2019-09-23T12:10:32Z', + // reason: 'ProgressDeadlineExceeded', + // message: 'replication controller "perl-1" has failed progressing', + // }, + // ], + // }, + }, + ], + }, + deployments: { + loaded: true, + loadError: '', + data: [ + { + kind: 'Deployment', + apiVersion: 'apps/v1', + metadata: { + annotations: { 'deployment.kubernetes.io/revision': '1' }, + resourceVersion: '471849', + name: 'test-deployment-1', + uid: '64b34874-debd-11e9-8cdf-0a0700ae5e38', + creationTimestamp: '2019-09-24T11:21:03Z', + generation: 4, + namespace: 'jeff-project', + labels: { 'app.kubernetes.io/part-of': 'application-3' }, + }, + // spec: { + // replicas: 6, + // selector: { matchLabels: { app: 'hello-openshift' } }, + // template: { + // metadata: { creationTimestamp: null, labels: { app: 'hello-openshift' } }, + // spec: { + // containers: [ + // { + // name: 'hello-openshift', + // image: 'openshift/hello-openshift', + // ports: [{ containerPort: 8080, protocol: 'TCP' }], + // resources: {}, + // terminationMessagePath: '/dev/termination-log', + // terminationMessagePolicy: 'File', + // imagePullPolicy: ImagePullPolicy.Always, + // }, + // ], + // restartPolicy: 'Always', + // terminationGracePeriodSeconds: 30, + // dnsPolicy: 'ClusterFirst', + // securityContext: {}, + // schedulerName: 'default-scheduler', + // }, + // }, + // strategy: { + // type: 'RollingUpdate', + // rollingUpdate: { maxUnavailable: '25%', maxSurge: '25%' }, + // }, + // revisionHistoryLimit: 10, + // progressDeadlineSeconds: 600, + // }, + // status: { + // observedGeneration: 4, + // replicas: 6, + // updatedReplicas: 6, + // readyReplicas: 6, + // availableReplicas: 6, + // conditions: [ + // { + // type: 'Progressing', + // status: 'True', + // lastUpdateTime: '2019-09-24T11:21:14Z', + // lastTransitionTime: '2019-09-24T11:21:03Z', + // reason: 'NewReplicaSetAvailable', + // message: 'ReplicaSet "test-deployment-1-54b47fbb75" has successfully progressed.', + // }, + // { + // type: 'Available', + // status: 'True', + // lastUpdateTime: '2019-09-24T11:24:57Z', + // lastTransitionTime: '2019-09-24T11:24:57Z', + // reason: 'MinimumReplicasAvailable', + // message: 'Deployment has minimum availability.', + // }, + // ], + // }, + }, + ], + }, + daemonSets: { + loaded: true, + loadError: '', + data: [ + { + kind: 'daemonset', + metadata: { + annotations: { + 'app.openshift.io/connects-to': 'test-deployment-1', + 'deprecated.daemonset.template.generation': '1', + }, + resourceVersion: '471360', + name: 'test-daemonset-1', + uid: '909bc726-debd-11e9-b95e-02de4f087472', + creationTimestamp: '2019-09-24T11:22:16Z', + generation: 1, + namespace: 'jeff-project', + labels: { 'app.kubernetes.io/part-of': 'application-2' }, + }, + // spec: { + // selector: { matchLabels: { app: 'hello-openshift' } }, + // template: { + // metadata: { creationTimestamp: null, labels: { app: 'hello-openshift' } }, + // spec: { + // containers: [ + // { + // name: 'hello-openshift', + // image: 'openshift/hello-openshift', + // ports: [{ containerPort: 8080, protocol: 'TCP' }], + // resources: {}, + // terminationMessagePath: '/dev/termination-log', + // terminationMessagePolicy: 'File', + // imagePullPolicy: ImagePullPolicy.Always, + // }, + // ], + // restartPolicy: 'Always', + // terminationGracePeriodSeconds: 30, + // dnsPolicy: 'ClusterFirst', + // securityContext: {}, + // schedulerName: 'default-scheduler', + // }, + // }, + // updateStrategy: { type: 'RollingUpdate', rollingUpdate: { maxUnavailable: 1 } }, + // revisionHistoryLimit: 10, + // }, + // status: { + // currentNumberScheduled: 3, + // numberMisscheduled: 0, + // desiredNumberScheduled: 3, + // numberReady: 3, + // observedGeneration: 1, + // updatedNumberScheduled: 3, + // numberAvailable: 3, + // }, + }, + ], + }, + pods: { + loaded: true, + loadError: '', + data: [ + { + apiVersion: 'v1', + kind: 'Pod', + metadata: { + generateName: 'test-deployment-1-54b47fbb75-', + annotations: { + 'k8s.v1.cni.cncf.io/networks-status': + '[{\n "name": "openshift-sdn",\n "interface": "eth0",\n "ips": [\n "10.128.2.20"\n ],\n "default": true,\n "dns": {}\n}]', + 'openshift.io/scc': 'restricted', + }, + resourceVersion: '470619', + name: 'test-deployment-1-54b47fbb75-d4sg7', + uid: '64b9cf32-debd-11e9-b22b-06b197463f30', + creationTimestamp: '2019-09-24T11:21:03Z', + namespace: 'jeff-project', + ownerReferences: [ + { + apiVersion: 'apps/v1', + kind: 'ReplicaSet', + name: 'test-deployment-1-54b47fbb75', + uid: '64b4abd7-debd-11e9-b22b-06b197463f30', + controller: true, + blockOwnerDeletion: true, + }, + ], + labels: { app: 'hello-openshift', 'pod-template-hash': '54b47fbb75' }, + }, + // spec: { + // restartPolicy: 'Always', + // serviceAccountName: 'default', + // imagePullSecrets: [{ name: 'default-dockercfg-zch2x' }], + // priority: 0, + // schedulerName: 'default-scheduler', + // enableServiceLinks: true, + // terminationGracePeriodSeconds: 30, + // nodeName: 'ip-10-0-146-80.us-east-2.compute.internal', + // securityContext: { seLinuxOptions: { level: 's0:c23,c7' }, fsGroup: 1000520000 }, + // containers: [ + // { + // resources: {}, + // terminationMessagePath: '/dev/termination-log', + // name: 'hello-openshift', + // securityContext: { + // capabilities: { drop: ['KILL', 'MKNOD', 'SETGID', 'SETUID'] }, + // runAsUser: 1000520000, + // }, + // ports: [{ containerPort: 8080, protocol: 'TCP' }], + // imagePullPolicy: ImagePullPolicy.Always, + // volumeMounts: [ + // { + // name: 'default-token-5ffs4', + // readOnly: true, + // mountPath: '/var/run/secrets/kubernetes.io/serviceaccount', + // }, + // ], + // terminationMessagePolicy: 'File', + // image: 'openshift/hello-openshift', + // }, + // ], + // serviceAccount: 'default', + // volumes: [ + // { + // name: 'default-token-5ffs4', + // secret: { secretName: 'default-token-5ffs4', defaultMode: 420 }, + // }, + // ], + // dnsPolicy: 'ClusterFirst', + // tolerations: [ + // { + // key: 'node.kubernetes.io/not-ready', + // operator: 'Exists', + // effect: 'NoExecute', + // tolerationSeconds: 300, + // }, + // { + // key: 'node.kubernetes.io/unreachable', + // operator: 'Exists', + // effect: 'NoExecute', + // tolerationSeconds: 300, + // }, + // ], + // }, + // status: { + // phase: 'Running', + // conditions: [ + // { + // type: 'Initialized', + // status: 'True', + // lastProbeTime: null, + // lastTransitionTime: '2019-09-24T11:21:03Z', + // }, + // { + // type: 'Ready', + // status: 'True', + // lastProbeTime: null, + // lastTransitionTime: '2019-09-24T11:21:13Z', + // }, + // { + // type: 'ContainersReady', + // status: 'True', + // lastProbeTime: null, + // lastTransitionTime: '2019-09-24T11:21:13Z', + // }, + // { + // type: 'PodScheduled', + // status: 'True', + // lastProbeTime: null, + // lastTransitionTime: '2019-09-24T11:21:03Z', + // }, + // ], + // hostIP: '10.0.146.80', + // podIP: '10.128.2.20', + // startTime: '2019-09-24T11:21:03Z', + // containerStatuses: [ + // { + // name: 'hello-openshift', + // state: { running: { startedAt: '2019-09-24T11:21:13Z' } }, + // lastState: {}, + // ready: true, + // restartCount: 0, + // image: 'docker.io/openshift/hello-openshift:latest', + // imageID: + // 'docker.io/openshift/hello-openshift@sha256:aaea76ff622d2f8bcb32e538e7b3cd0ef6d291953f3e7c9f556c1ba5baf47e2e', + // containerID: + // 'cri-o://2128884957cce2cbb56602e3b029c173dc3c42a7c0629454bfc2868a596111bd', + // }, + // ], + // qosClass: 'BestEffort', + // }, + }, + { + kind: 'Pod', + apiVersion: 'v1', + metadata: { + generateName: 'test-deployment-1-54b47fbb75-', + annotations: { + 'k8s.v1.cni.cncf.io/networks-status': + '[{\n "name": "openshift-sdn",\n "interface": "eth0",\n "ips": [\n "10.128.2.22"\n ],\n "default": true,\n "dns": {}\n}]', + 'openshift.io/scc': 'restricted', + }, + resourceVersion: '471828', + name: 'test-deployment-1-54b47fbb75-cfrkq', + uid: 'eb11d324-debd-11e9-b22b-06b197463f30', + creationTimestamp: '2019-09-24T11:24:48Z', + namespace: 'jeff-project', + ownerReferences: [ + { + apiVersion: 'apps/v1', + kind: 'ReplicaSet', + name: 'test-deployment-1-54b47fbb75', + uid: '64b4abd7-debd-11e9-b22b-06b197463f30', + controller: true, + blockOwnerDeletion: true, + }, + ], + labels: { app: 'hello-openshift', 'pod-template-hash': '54b47fbb75' }, + }, + // spec: { + // restartPolicy: 'Always', + // serviceAccountName: 'default', + // imagePullSecrets: [{ name: 'default-dockercfg-zch2x' }], + // priority: 0, + // schedulerName: 'default-scheduler', + // enableServiceLinks: true, + // terminationGracePeriodSeconds: 30, + // nodeName: 'ip-10-0-146-80.us-east-2.compute.internal', + // securityContext: { seLinuxOptions: { level: 's0:c23,c7' }, fsGroup: 1000520000 }, + // containers: [ + // { + // resources: {}, + // terminationMessagePath: '/dev/termination-log', + // name: 'hello-openshift', + // securityContext: { + // capabilities: { drop: ['KILL', 'MKNOD', 'SETGID', 'SETUID'] }, + // runAsUser: 1000520000, + // }, + // ports: [{ containerPort: 8080, protocol: 'TCP' }], + // imagePullPolicy: ImagePullPolicy.Always, + // volumeMounts: [ + // { + // name: 'default-token-5ffs4', + // readOnly: true, + // mountPath: '/var/run/secrets/kubernetes.io/serviceaccount', + // }, + // ], + // terminationMessagePolicy: 'File', + // image: 'openshift/hello-openshift', + // }, + // ], + // serviceAccount: 'default', + // volumes: [ + // { + // name: 'default-token-5ffs4', + // secret: { secretName: 'default-token-5ffs4', defaultMode: 420 }, + // }, + // ], + // dnsPolicy: 'ClusterFirst', + // tolerations: [ + // { + // key: 'node.kubernetes.io/not-ready', + // operator: 'Exists', + // effect: 'NoExecute', + // tolerationSeconds: 300, + // }, + // { + // key: 'node.kubernetes.io/unreachable', + // operator: 'Exists', + // effect: 'NoExecute', + // tolerationSeconds: 300, + // }, + // ], + // }, + // status: { + // phase: 'Running', + // conditions: [ + // { + // type: 'Initialized', + // status: 'True', + // lastProbeTime: null, + // lastTransitionTime: '2019-09-24T11:24:48Z', + // }, + // { + // type: 'Ready', + // status: 'True', + // lastProbeTime: null, + // lastTransitionTime: '2019-09-24T11:24:56Z', + // }, + // { + // type: 'ContainersReady', + // status: 'True', + // lastProbeTime: null, + // lastTransitionTime: '2019-09-24T11:24:56Z', + // }, + // { + // type: 'PodScheduled', + // status: 'True', + // lastProbeTime: null, + // lastTransitionTime: '2019-09-24T11:24:48Z', + // }, + // ], + // hostIP: '10.0.146.80', + // podIP: '10.128.2.22', + // startTime: '2019-09-24T11:24:48Z', + // containerStatuses: [ + // { + // name: 'hello-openshift', + // state: { running: { startedAt: '2019-09-24T11:24:56Z' } }, + // lastState: {}, + // ready: true, + // restartCount: 0, + // image: 'docker.io/openshift/hello-openshift:latest', + // imageID: + // 'docker.io/openshift/hello-openshift@sha256:aaea76ff622d2f8bcb32e538e7b3cd0ef6d291953f3e7c9f556c1ba5baf47e2e', + // containerID: + // 'cri-o://9e9c7a447ed17f709c11447fbdbd011c260cbbe1004d67dcc1667885ae42de07', + // }, + // ], + // qosClass: 'BestEffort', + // }, + }, + { + apiVersion: 'v1', + kind: 'Pod', + metadata: { + generateName: 'test-daemonset-1-', + annotations: { + 'k8s.v1.cni.cncf.io/networks-status': + '[{\n "name": "openshift-sdn",\n "interface": "eth0",\n "ips": [\n "10.128.2.21"\n ],\n "default": true,\n "dns": {}\n}]', + 'openshift.io/scc': 'restricted', + }, + resourceVersion: '471027', + name: 'test-daemonset-1-ngjfn', + uid: '90a149a4-debd-11e9-b22b-06b197463f30', + creationTimestamp: '2019-09-24T11:22:16Z', + namespace: 'jeff-project', + ownerReferences: [ + { + apiVersion: 'apps/v1', + kind: 'DaemonSet', + name: 'test-daemonset-1', + uid: '909bc726-debd-11e9-b95e-02de4f087472', + controller: true, + blockOwnerDeletion: true, + }, + ], + labels: { + app: 'hello-openshift', + 'controller-revision-hash': '54b47fbb75', + 'pod-template-generation': '1', + }, + }, + // spec: { + // restartPolicy: 'Always', + // serviceAccountName: 'default', + // imagePullSecrets: [{ name: 'default-dockercfg-zch2x' }], + // priority: 0, + // schedulerName: 'default-scheduler', + // enableServiceLinks: true, + // affinity: { + // nodeAffinity: { + // requiredDuringSchedulingIgnoredDuringExecution: { + // nodeSelectorTerms: [ + // { + // matchFields: [ + // { + // key: 'metadata.name', + // operator: 'In', + // values: ['ip-10-0-146-80.us-east-2.compute.internal'], + // }, + // ], + // }, + // ], + // }, + // }, + // }, + // terminationGracePeriodSeconds: 30, + // nodeName: 'ip-10-0-146-80.us-east-2.compute.internal', + // securityContext: { seLinuxOptions: { level: 's0:c23,c7' }, fsGroup: 1000520000 }, + // containers: [ + // { + // resources: {}, + // terminationMessagePath: '/dev/termination-log', + // name: 'hello-openshift', + // securityContext: { + // capabilities: { drop: ['KILL', 'MKNOD', 'SETGID', 'SETUID'] }, + // runAsUser: 1000520000, + // }, + // ports: [{ containerPort: 8080, protocol: 'TCP' }], + // imagePullPolicy: ImagePullPolicy.Always, + // volumeMounts: [ + // { + // name: 'default-token-5ffs4', + // readOnly: true, + // mountPath: '/var/run/secrets/kubernetes.io/serviceaccount', + // }, + // ], + // terminationMessagePolicy: 'File', + // image: 'openshift/hello-openshift', + // }, + // ], + // serviceAccount: 'default', + // volumes: [ + // { + // name: 'default-token-5ffs4', + // secret: { secretName: 'default-token-5ffs4', defaultMode: 420 }, + // }, + // ], + // dnsPolicy: 'ClusterFirst', + // tolerations: [ + // { key: 'node.kubernetes.io/unschedulable', operator: 'Exists', effect: 'NoSchedule' }, + // { key: 'node.kubernetes.io/not-ready', operator: 'Exists', effect: 'NoExecute' }, + // { key: 'node.kubernetes.io/unreachable', operator: 'Exists', effect: 'NoExecute' }, + // { key: 'node.kubernetes.io/disk-pressure', operator: 'Exists', effect: 'NoSchedule' }, + // { key: 'node.kubernetes.io/memory-pressure', operator: 'Exists', effect: 'NoSchedule' }, + // { key: 'node.kubernetes.io/pid-pressure', operator: 'Exists', effect: 'NoSchedule' }, + // ], + // }, + // status: { + // phase: 'Running', + // conditions: [ + // { + // type: 'Initialized', + // status: 'True', + // lastProbeTime: null, + // lastTransitionTime: '2019-09-24T11:22:16Z', + // }, + // { + // type: 'Ready', + // status: 'True', + // lastProbeTime: null, + // lastTransitionTime: '2019-09-24T11:22:25Z', + // }, + // { + // type: 'ContainersReady', + // status: 'True', + // lastProbeTime: null, + // lastTransitionTime: '2019-09-24T11:22:25Z', + // }, + // { + // type: 'PodScheduled', + // status: 'True', + // lastProbeTime: null, + // lastTransitionTime: '2019-09-24T11:22:16Z', + // }, + // ], + // hostIP: '10.0.146.80', + // podIP: '10.128.2.21', + // startTime: '2019-09-24T11:22:16Z', + // containerStatuses: [ + // { + // name: 'hello-openshift', + // state: { running: { startedAt: '2019-09-24T11:22:24Z' } }, + // lastState: {}, + // ready: true, + // restartCount: 0, + // image: 'docker.io/openshift/hello-openshift:latest', + // imageID: + // 'docker.io/openshift/hello-openshift@sha256:aaea76ff622d2f8bcb32e538e7b3cd0ef6d291953f3e7c9f556c1ba5baf47e2e', + // containerID: + // 'cri-o://2b026c55fdeef70c73af03d19a5a4dc91b9c809a9c59b9fc8e754f9d3a1c89fd', + // }, + // ], + // qosClass: 'BestEffort', + // }, + }, + { + apiVersion: 'v1', + kind: 'Pod', + metadata: { + generateName: 'test-daemonset-1-', + annotations: { + 'k8s.v1.cni.cncf.io/networks-status': + '[{\n "name": "openshift-sdn",\n "interface": "eth0",\n "ips": [\n "10.129.2.20"\n ],\n "default": true,\n "dns": {}\n}]', + 'openshift.io/scc': 'restricted', + }, + resourceVersion: '471035', + name: 'test-daemonset-1-72zfc', + uid: '90a1d40a-debd-11e9-b22b-06b197463f30', + creationTimestamp: '2019-09-24T11:22:16Z', + namespace: 'jeff-project', + ownerReferences: [ + { + apiVersion: 'apps/v1', + kind: 'DaemonSet', + name: 'test-daemonset-1', + uid: '909bc726-debd-11e9-b95e-02de4f087472', + controller: true, + blockOwnerDeletion: true, + }, + ], + labels: { + app: 'hello-openshift', + 'controller-revision-hash': '54b47fbb75', + 'pod-template-generation': '1', + }, + }, + // spec: { + // restartPolicy: 'Always', + // serviceAccountName: 'default', + // imagePullSecrets: [{ name: 'default-dockercfg-zch2x' }], + // priority: 0, + // schedulerName: 'default-scheduler', + // enableServiceLinks: true, + // affinity: { + // nodeAffinity: { + // requiredDuringSchedulingIgnoredDuringExecution: { + // nodeSelectorTerms: [ + // { + // matchFields: [ + // { + // key: 'metadata.name', + // operator: 'In', + // values: ['ip-10-0-132-222.us-east-2.compute.internal'], + // }, + // ], + // }, + // ], + // }, + // }, + // }, + // terminationGracePeriodSeconds: 30, + // nodeName: 'ip-10-0-132-222.us-east-2.compute.internal', + // securityContext: { seLinuxOptions: { level: 's0:c23,c7' }, fsGroup: 1000520000 }, + // containers: [ + // { + // resources: {}, + // terminationMessagePath: '/dev/termination-log', + // name: 'hello-openshift', + // securityContext: { + // capabilities: { drop: ['KILL', 'MKNOD', 'SETGID', 'SETUID'] }, + // runAsUser: 1000520000, + // }, + // ports: [{ containerPort: 8080, protocol: 'TCP' }], + // imagePullPolicy: ImagePullPolicy.Always, + // volumeMounts: [ + // { + // name: 'default-token-5ffs4', + // readOnly: true, + // mountPath: '/var/run/secrets/kubernetes.io/serviceaccount', + // }, + // ], + // terminationMessagePolicy: 'File', + // image: 'openshift/hello-openshift', + // }, + // ], + // serviceAccount: 'default', + // volumes: [ + // { + // name: 'default-token-5ffs4', + // secret: { secretName: 'default-token-5ffs4', defaultMode: 420 }, + // }, + // ], + // dnsPolicy: 'ClusterFirst', + // tolerations: [ + // { key: 'node.kubernetes.io/not-ready', operator: 'Exists', effect: 'NoExecute' }, + // { key: 'node.kubernetes.io/unreachable', operator: 'Exists', effect: 'NoExecute' }, + // { key: 'node.kubernetes.io/disk-pressure', operator: 'Exists', effect: 'NoSchedule' }, + // { key: 'node.kubernetes.io/memory-pressure', operator: 'Exists', effect: 'NoSchedule' }, + // { key: 'node.kubernetes.io/pid-pressure', operator: 'Exists', effect: 'NoSchedule' }, + // { key: 'node.kubernetes.io/unschedulable', operator: 'Exists', effect: 'NoSchedule' }, + // ], + // }, + // status: { + // phase: 'Running', + // conditions: [ + // { + // type: 'Initialized', + // status: 'True', + // lastProbeTime: null, + // lastTransitionTime: '2019-09-24T11:22:16Z', + // }, + // { + // type: 'Ready', + // status: 'True', + // lastProbeTime: null, + // lastTransitionTime: '2019-09-24T11:22:25Z', + // }, + // { + // type: 'ContainersReady', + // status: 'True', + // lastProbeTime: null, + // lastTransitionTime: '2019-09-24T11:22:25Z', + // }, + // { + // type: 'PodScheduled', + // status: 'True', + // lastProbeTime: null, + // lastTransitionTime: '2019-09-24T11:22:16Z', + // }, + // ], + // hostIP: '10.0.132.222', + // podIP: '10.129.2.20', + // startTime: '2019-09-24T11:22:16Z', + // containerStatuses: [ + // { + // name: 'hello-openshift', + // state: { running: { startedAt: '2019-09-24T11:22:25Z' } }, + // lastState: {}, + // ready: true, + // restartCount: 0, + // image: 'docker.io/openshift/hello-openshift:latest', + // imageID: + // 'docker.io/openshift/hello-openshift@sha256:aaea76ff622d2f8bcb32e538e7b3cd0ef6d291953f3e7c9f556c1ba5baf47e2e', + // containerID: + // 'cri-o://c8a01ca3983265a7ff527b9179d44cad9032a6fe9748a890c22327a240bacf80', + // }, + // ], + // qosClass: 'BestEffort', + // }, + }, + { + apiVersion: 'v1', + kind: 'Pod', + metadata: { + annotations: { + 'k8s.v1.cni.cncf.io/networks-status': '', + 'openshift.io/deployment-config.name': 'perl', + 'openshift.io/deployment.name': 'perl-1', + 'openshift.io/scc': 'restricted', + }, + resourceVersion: '29637', + name: 'perl-1-deploy', + uid: 'b75a3a67-ddf9-11e9-8d63-02de4f087472', + creationTimestamp: '2019-09-23T12:00:20Z', + namespace: 'jeff-project', + ownerReferences: [ + { + apiVersion: 'v1', + kind: 'ReplicationController', + name: 'perl-1', + uid: 'b74b9c2b-ddf9-11e9-8d63-02de4f087472', + }, + ], + labels: { 'openshift.io/deployer-pod-for.name': 'perl-1' }, + }, + // spec: { + // restartPolicy: 'Never', + // activeDeadlineSeconds: 21600, + // serviceAccountName: 'deployer', + // imagePullSecrets: [{ name: 'deployer-dockercfg-5gd7c' }], + // priority: 0, + // schedulerName: 'default-scheduler', + // enableServiceLinks: true, + // terminationGracePeriodSeconds: 10, + // shareProcessNamespace: false, + // nodeName: 'ip-10-0-132-222.us-east-2.compute.internal', + // securityContext: { seLinuxOptions: { level: 's0:c23,c7' }, fsGroup: 1000520000 }, + // containers: [ + // { + // resources: {}, + // terminationMessagePath: '/dev/termination-log', + // name: 'deployment', + // env: [ + // { name: 'OPENSHIFT_DEPLOYMENT_NAME', value: 'perl-1' }, + // { name: 'OPENSHIFT_DEPLOYMENT_NAMESPACE', value: 'jeff-project' }, + // ], + // securityContext: { + // capabilities: { drop: ['KILL', 'MKNOD', 'SETGID', 'SETUID'] }, + // runAsUser: 1000520000, + // }, + // imagePullPolicy: ImagePullPolicy.IfNotPresent, + // volumeMounts: [ + // { + // name: 'deployer-token-x876x', + // readOnly: true, + // mountPath: '/var/run/secrets/kubernetes.io/serviceaccount', + // }, + // ], + // terminationMessagePolicy: 'File', + // image: + // 'registry.svc.ci.openshift.org/origin/4.2-2019-09-23-102648@sha256:60f9bf5d8a9ec601e099f62a853642bb453bd31427712e43efe6de793b10b869', + // }, + // ], + // serviceAccount: 'deployer', + // volumes: [ + // { + // name: 'deployer-token-x876x', + // secret: { secretName: 'deployer-token-x876x', defaultMode: 420 }, + // }, + // ], + // dnsPolicy: 'ClusterFirst', + // tolerations: [ + // { + // key: 'node.kubernetes.io/not-ready', + // operator: 'Exists', + // effect: 'NoExecute', + // tolerationSeconds: 300, + // }, + // { + // key: 'node.kubernetes.io/unreachable', + // operator: 'Exists', + // effect: 'NoExecute', + // tolerationSeconds: 300, + // }, + // ], + // }, + // status: { + // phase: 'Failed', + // conditions: [ + // { + // type: 'Initialized', + // status: 'True', + // lastProbeTime: null, + // lastTransitionTime: '2019-09-23T12:00:20Z', + // }, + // { + // type: 'Ready', + // status: 'False', + // lastProbeTime: null, + // lastTransitionTime: '2019-09-23T12:10:32Z', + // reason: 'ContainersNotReady', + // message: 'containers with unready status: [deployment]', + // }, + // { + // type: 'ContainersReady', + // status: 'False', + // lastProbeTime: null, + // lastTransitionTime: '2019-09-23T12:10:32Z', + // reason: 'ContainersNotReady', + // message: 'containers with unready status: [deployment]', + // }, + // { + // type: 'PodScheduled', + // status: 'True', + // lastProbeTime: null, + // lastTransitionTime: '2019-09-23T12:00:20Z', + // }, + // ], + // hostIP: '10.0.132.222', + // podIP: '10.129.2.11', + // startTime: '2019-09-23T12:00:20Z', + // containerStatuses: [ + // { + // name: 'deployment', + // state: { + // terminated: { + // exitCode: 1, + // reason: 'Error', + // startedAt: '2019-09-23T12:00:31Z', + // finishedAt: '2019-09-23T12:10:32Z', + // containerID: + // 'cri-o://6b481bc1d2635ea50f69e86f71b32888c750d6493078873efd2c815726daf117', + // }, + // }, + // lastState: {}, + // ready: false, + // restartCount: 0, + // image: + // 'registry.svc.ci.openshift.org/origin/4.2-2019-09-23-102648@sha256:60f9bf5d8a9ec601e099f62a853642bb453bd31427712e43efe6de793b10b869', + // imageID: + // 'registry.svc.ci.openshift.org/origin/4.2-2019-09-23-102648@sha256:60f9bf5d8a9ec601e099f62a853642bb453bd31427712e43efe6de793b10b869', + // containerID: + // 'cri-o://6b481bc1d2635ea50f69e86f71b32888c750d6493078873efd2c815726daf117', + // }, + // ], + // qosClass: 'BestEffort', + // }, + }, + { + apiVersion: 'v1', + kind: 'Pod', + metadata: { + generateName: 'test-daemonset-1-', + annotations: { + 'k8s.v1.cni.cncf.io/networks-status': + '[{\n "name": "openshift-sdn",\n "interface": "eth0",\n "ips": [\n "10.131.0.21"\n ],\n "default": true,\n "dns": {}\n}]', + 'openshift.io/scc': 'restricted', + }, + resourceVersion: '471041', + name: 'test-daemonset-1-hpjv7', + uid: '909f68be-debd-11e9-b22b-06b197463f30', + creationTimestamp: '2019-09-24T11:22:16Z', + namespace: 'jeff-project', + ownerReferences: [ + { + apiVersion: 'apps/v1', + kind: 'DaemonSet', + name: 'test-daemonset-1', + uid: '909bc726-debd-11e9-b95e-02de4f087472', + controller: true, + blockOwnerDeletion: true, + }, + ], + labels: { + app: 'hello-openshift', + 'controller-revision-hash': '54b47fbb75', + 'pod-template-generation': '1', + }, + }, + // spec: { + // restartPolicy: 'Always', + // serviceAccountName: 'default', + // imagePullSecrets: [{ name: 'default-dockercfg-zch2x' }], + // priority: 0, + // schedulerName: 'default-scheduler', + // enableServiceLinks: true, + // affinity: { + // nodeAffinity: { + // requiredDuringSchedulingIgnoredDuringExecution: { + // nodeSelectorTerms: [ + // { + // matchFields: [ + // { + // key: 'metadata.name', + // operator: 'In', + // values: ['ip-10-0-168-116.us-east-2.compute.internal'], + // }, + // ], + // }, + // ], + // }, + // }, + // }, + // terminationGracePeriodSeconds: 30, + // nodeName: 'ip-10-0-168-116.us-east-2.compute.internal', + // securityContext: { seLinuxOptions: { level: 's0:c23,c7' }, fsGroup: 1000520000 }, + // containers: [ + // { + // resources: {}, + // terminationMessagePath: '/dev/termination-log', + // name: 'hello-openshift', + // securityContext: { + // capabilities: { drop: ['KILL', 'MKNOD', 'SETGID', 'SETUID'] }, + // runAsUser: 1000520000, + // }, + // ports: [{ containerPort: 8080, protocol: 'TCP' }], + // imagePullPolicy: ImagePullPolicy.Always, + // volumeMounts: [ + // { + // name: 'default-token-5ffs4', + // readOnly: true, + // mountPath: '/var/run/secrets/kubernetes.io/serviceaccount', + // }, + // ], + // terminationMessagePolicy: 'File', + // image: 'openshift/hello-openshift', + // }, + // ], + // serviceAccount: 'default', + // volumes: [ + // { + // name: 'default-token-5ffs4', + // secret: { secretName: 'default-token-5ffs4', defaultMode: 420 }, + // }, + // ], + // dnsPolicy: 'ClusterFirst', + // tolerations: [ + // { key: 'node.kubernetes.io/not-ready', operator: 'Exists', effect: 'NoExecute' }, + // { key: 'node.kubernetes.io/unreachable', operator: 'Exists', effect: 'NoExecute' }, + // { key: 'node.kubernetes.io/disk-pressure', operator: 'Exists', effect: 'NoSchedule' }, + // { key: 'node.kubernetes.io/memory-pressure', operator: 'Exists', effect: 'NoSchedule' }, + // { key: 'node.kubernetes.io/pid-pressure', operator: 'Exists', effect: 'NoSchedule' }, + // { key: 'node.kubernetes.io/unschedulable', operator: 'Exists', effect: 'NoSchedule' }, + // ], + // }, + // status: { + // phase: 'Running', + // conditions: [ + // { + // type: 'Initialized', + // status: 'True', + // lastProbeTime: null, + // lastTransitionTime: '2019-09-24T11:22:16Z', + // }, + // { + // type: 'Ready', + // status: 'True', + // lastProbeTime: null, + // lastTransitionTime: '2019-09-24T11:22:26Z', + // }, + // { + // type: 'ContainersReady', + // status: 'True', + // lastProbeTime: null, + // lastTransitionTime: '2019-09-24T11:22:26Z', + // }, + // { + // type: 'PodScheduled', + // status: 'True', + // lastProbeTime: null, + // lastTransitionTime: '2019-09-24T11:22:16Z', + // }, + // ], + // hostIP: '10.0.168.116', + // podIP: '10.131.0.21', + // startTime: '2019-09-24T11:22:16Z', + // containerStatuses: [ + // { + // name: 'hello-openshift', + // state: { running: { startedAt: '2019-09-24T11:22:25Z' } }, + // lastState: {}, + // ready: true, + // restartCount: 0, + // image: 'docker.io/openshift/hello-openshift:latest', + // imageID: + // 'docker.io/openshift/hello-openshift@sha256:aaea76ff622d2f8bcb32e538e7b3cd0ef6d291953f3e7c9f556c1ba5baf47e2e', + // containerID: + // 'cri-o://6b34a4e1e6f25ae6e763b144087bedd88cd021c4aca944a2b4ecbe223f79e8d8', + // }, + // ], + // qosClass: 'BestEffort', + // }, + }, + { + kind: 'Pod', + apiVersion: 'v1', + metadata: { + generateName: 'test-deployment-1-54b47fbb75-', + annotations: { + 'k8s.v1.cni.cncf.io/networks-status': + '[{\n "name": "openshift-sdn",\n "interface": "eth0",\n "ips": [\n "10.131.0.22"\n ],\n "default": true,\n "dns": {}\n}]', + 'openshift.io/scc': 'restricted', + }, + resourceVersion: '471840', + name: 'test-deployment-1-54b47fbb75-rbf4d', + uid: 'eb44c495-debd-11e9-b22b-06b197463f30', + creationTimestamp: '2019-09-24T11:24:48Z', + namespace: 'jeff-project', + ownerReferences: [ + { + apiVersion: 'apps/v1', + kind: 'ReplicaSet', + name: 'test-deployment-1-54b47fbb75', + uid: '64b4abd7-debd-11e9-b22b-06b197463f30', + controller: true, + blockOwnerDeletion: true, + }, + ], + labels: { app: 'hello-openshift', 'pod-template-hash': '54b47fbb75' }, + }, + // spec: { + // restartPolicy: 'Always', + // serviceAccountName: 'default', + // imagePullSecrets: [{ name: 'default-dockercfg-zch2x' }], + // priority: 0, + // schedulerName: 'default-scheduler', + // enableServiceLinks: true, + // terminationGracePeriodSeconds: 30, + // nodeName: 'ip-10-0-168-116.us-east-2.compute.internal', + // securityContext: { seLinuxOptions: { level: 's0:c23,c7' }, fsGroup: 1000520000 }, + // containers: [ + // { + // resources: {}, + // terminationMessagePath: '/dev/termination-log', + // name: 'hello-openshift', + // securityContext: { + // capabilities: { drop: ['KILL', 'MKNOD', 'SETGID', 'SETUID'] }, + // runAsUser: 1000520000, + // }, + // ports: [{ containerPort: 8080, protocol: 'TCP' }], + // imagePullPolicy: ImagePullPolicy.Always, + // volumeMounts: [ + // { + // name: 'default-token-5ffs4', + // readOnly: true, + // mountPath: '/var/run/secrets/kubernetes.io/serviceaccount', + // }, + // ], + // terminationMessagePolicy: 'File', + // image: 'openshift/hello-openshift', + // }, + // ], + // serviceAccount: 'default', + // volumes: [ + // { + // name: 'default-token-5ffs4', + // secret: { secretName: 'default-token-5ffs4', defaultMode: 420 }, + // }, + // ], + // dnsPolicy: 'ClusterFirst', + // tolerations: [ + // { + // key: 'node.kubernetes.io/not-ready', + // operator: 'Exists', + // effect: 'NoExecute', + // tolerationSeconds: 300, + // }, + // { + // key: 'node.kubernetes.io/unreachable', + // operator: 'Exists', + // effect: 'NoExecute', + // tolerationSeconds: 300, + // }, + // ], + // }, + // status: { + // phase: 'Running', + // conditions: [ + // { + // type: 'Initialized', + // status: 'True', + // lastProbeTime: null, + // lastTransitionTime: '2019-09-24T11:24:48Z', + // }, + // { + // type: 'Ready', + // status: 'True', + // lastProbeTime: null, + // lastTransitionTime: '2019-09-24T11:24:57Z', + // }, + // { + // type: 'ContainersReady', + // status: 'True', + // lastProbeTime: null, + // lastTransitionTime: '2019-09-24T11:24:57Z', + // }, + // { + // type: 'PodScheduled', + // status: 'True', + // lastProbeTime: null, + // lastTransitionTime: '2019-09-24T11:24:48Z', + // }, + // ], + // hostIP: '10.0.168.116', + // podIP: '10.131.0.22', + // startTime: '2019-09-24T11:24:48Z', + // containerStatuses: [ + // { + // name: 'hello-openshift', + // state: { running: { startedAt: '2019-09-24T11:24:57Z' } }, + // lastState: {}, + // ready: true, + // restartCount: 0, + // image: 'docker.io/openshift/hello-openshift:latest', + // imageID: + // 'docker.io/openshift/hello-openshift@sha256:aaea76ff622d2f8bcb32e538e7b3cd0ef6d291953f3e7c9f556c1ba5baf47e2e', + // containerID: + // 'cri-o://1a2b19fa9691675191f04122f68365a9fff51073a8e7b563e3cef8d6d6c9b0dc', + // }, + // ], + // qosClass: 'BestEffort', + // }, + }, + { + apiVersion: 'v1', + kind: 'Pod', + metadata: { + generateName: 'test-statefulset-1-', + annotations: { 'openshift.io/scc': 'restricted' }, + resourceVersion: '470712', + name: 'test-statefulset-1-0', + uid: '75c306e4-debd-11e9-b22b-06b197463f30', + creationTimestamp: '2019-09-24T11:21:31Z', + namespace: 'jeff-project', + ownerReferences: [ + { + apiVersion: 'apps/v1', + kind: 'StatefulSet', + name: 'test-statefulset-1', + uid: '75c049b5-debd-11e9-8cdf-0a0700ae5e38', + controller: true, + blockOwnerDeletion: true, + }, + ], + labels: { + app: 'httpd', + 'controller-revision-hash': 'test-statefulset-1-7d57df7bfc', + 'statefulset.kubernetes.io/pod-name': 'test-statefulset-1-0', + }, + }, + // spec: { + // restartPolicy: 'Always', + // serviceAccountName: 'default', + // imagePullSecrets: [{ name: 'default-dockercfg-zch2x' }], + // priority: 0, + // subdomain: 'httpd', + // schedulerName: 'default-scheduler', + // enableServiceLinks: true, + // terminationGracePeriodSeconds: 10, + // securityContext: { seLinuxOptions: { level: 's0:c23,c7' }, fsGroup: 1000520000 }, + // containers: [ + // { + // resources: {}, + // terminationMessagePath: '/dev/termination-log', + // name: 'httpd', + // securityContext: { + // capabilities: { drop: ['KILL', 'MKNOD', 'SETGID', 'SETUID'] }, + // runAsUser: 1000520000, + // }, + // ports: [{ name: 'web', containerPort: 8080, protocol: 'TCP' }], + // imagePullPolicy: ImagePullPolicy.IfNotPresent, + // volumeMounts: [ + // { name: 'www', mountPath: '/var/www/html' }, + // { + // name: 'default-token-5ffs4', + // readOnly: true, + // mountPath: '/var/run/secrets/kubernetes.io/serviceaccount', + // }, + // ], + // terminationMessagePolicy: 'File', + // image: 'image-registry.openshift-image-registry.svc:5000/openshift/httpd:latest', + // }, + // ], + // hostname: 'test-statefulset-1-0', + // serviceAccount: 'default', + // volumes: [ + // { name: 'www', persistentVolumeClaim: { claimName: 'www-test-statefulset-1-0' } }, + // { + // name: 'default-token-5ffs4', + // secret: { secretName: 'default-token-5ffs4', defaultMode: 420 }, + // }, + // ], + // dnsPolicy: 'ClusterFirst', + // tolerations: [ + // { + // key: 'node.kubernetes.io/not-ready', + // operator: 'Exists', + // effect: 'NoExecute', + // tolerationSeconds: 300, + // }, + // { + // key: 'node.kubernetes.io/unreachable', + // operator: 'Exists', + // effect: 'NoExecute', + // tolerationSeconds: 300, + // }, + // ], + // }, + // status: { + // phase: 'Pending', + // conditions: [ + // { + // type: 'PodScheduled', + // status: 'False', + // lastProbeTime: null, + // lastTransitionTime: '2019-09-24T11:21:31Z', + // reason: 'Unschedulable', + // message: 'pod has unbound immediate PersistentVolumeClaims (repeated 3 times)', + // }, + // ], + // qosClass: 'BestEffort', + // }, + }, + { + kind: 'Pod', + apiVersion: 'v1', + metadata: { + generateName: 'test-deployment-1-54b47fbb75-', + annotations: { + 'k8s.v1.cni.cncf.io/networks-status': + '[{\n "name": "openshift-sdn",\n "interface": "eth0",\n "ips": [\n "10.129.2.21"\n ],\n "default": true,\n "dns": {}\n}]', + 'openshift.io/scc': 'restricted', + }, + resourceVersion: '471846', + name: 'test-deployment-1-54b47fbb75-whl8h', + uid: 'eb264e6e-debd-11e9-b22b-06b197463f30', + creationTimestamp: '2019-09-24T11:24:48Z', + namespace: 'jeff-project', + ownerReferences: [ + { + apiVersion: 'apps/v1', + kind: 'ReplicaSet', + name: 'test-deployment-1-54b47fbb75', + uid: '64b4abd7-debd-11e9-b22b-06b197463f30', + controller: true, + blockOwnerDeletion: true, + }, + ], + labels: { app: 'hello-openshift', 'pod-template-hash': '54b47fbb75' }, + }, + // spec: { + // restartPolicy: 'Always', + // serviceAccountName: 'default', + // imagePullSecrets: [{ name: 'default-dockercfg-zch2x' }], + // priority: 0, + // schedulerName: 'default-scheduler', + // enableServiceLinks: true, + // terminationGracePeriodSeconds: 30, + // nodeName: 'ip-10-0-132-222.us-east-2.compute.internal', + // securityContext: { seLinuxOptions: { level: 's0:c23,c7' }, fsGroup: 1000520000 }, + // containers: [ + // { + // resources: {}, + // terminationMessagePath: '/dev/termination-log', + // name: 'hello-openshift', + // securityContext: { + // capabilities: { drop: ['KILL', 'MKNOD', 'SETGID', 'SETUID'] }, + // runAsUser: 1000520000, + // }, + // ports: [{ containerPort: 8080, protocol: 'TCP' }], + // imagePullPolicy: ImagePullPolicy.Always, + // volumeMounts: [ + // { + // name: 'default-token-5ffs4', + // readOnly: true, + // mountPath: '/var/run/secrets/kubernetes.io/serviceaccount', + // }, + // ], + // terminationMessagePolicy: 'File', + // image: 'openshift/hello-openshift', + // }, + // ], + // serviceAccount: 'default', + // volumes: [ + // { + // name: 'default-token-5ffs4', + // secret: { secretName: 'default-token-5ffs4', defaultMode: 420 }, + // }, + // ], + // dnsPolicy: 'ClusterFirst', + // tolerations: [ + // { + // key: 'node.kubernetes.io/not-ready', + // operator: 'Exists', + // effect: 'NoExecute', + // tolerationSeconds: 300, + // }, + // { + // key: 'node.kubernetes.io/unreachable', + // operator: 'Exists', + // effect: 'NoExecute', + // tolerationSeconds: 300, + // }, + // ], + // }, + // status: { + // phase: 'Running', + // conditions: [ + // { + // type: 'Initialized', + // status: 'True', + // lastProbeTime: null, + // lastTransitionTime: '2019-09-24T11:24:48Z', + // }, + // { + // type: 'Ready', + // status: 'True', + // lastProbeTime: null, + // lastTransitionTime: '2019-09-24T11:24:57Z', + // }, + // { + // type: 'ContainersReady', + // status: 'True', + // lastProbeTime: null, + // lastTransitionTime: '2019-09-24T11:24:57Z', + // }, + // { + // type: 'PodScheduled', + // status: 'True', + // lastProbeTime: null, + // lastTransitionTime: '2019-09-24T11:24:48Z', + // }, + // ], + // hostIP: '10.0.132.222', + // podIP: '10.129.2.21', + // startTime: '2019-09-24T11:24:48Z', + // containerStatuses: [ + // { + // name: 'hello-openshift', + // state: { running: { startedAt: '2019-09-24T11:24:57Z' } }, + // lastState: {}, + // ready: true, + // restartCount: 0, + // image: 'docker.io/openshift/hello-openshift:latest', + // imageID: + // 'docker.io/openshift/hello-openshift@sha256:aaea76ff622d2f8bcb32e538e7b3cd0ef6d291953f3e7c9f556c1ba5baf47e2e', + // containerID: + // 'cri-o://5c01809dcde7fd1caf8b440fc4d3591ed08607a1ac20701e299103c2b25ce9b8', + // }, + // ], + // qosClass: 'BestEffort', + // }, + }, + { + apiVersion: 'v1', + kind: 'Pod', + metadata: { + generateName: 'test-deployment-1-54b47fbb75-', + annotations: { + 'k8s.v1.cni.cncf.io/networks-status': + '[{\n "name": "openshift-sdn",\n "interface": "eth0",\n "ips": [\n "10.129.2.19"\n ],\n "default": true,\n "dns": {}\n}]', + 'openshift.io/scc': 'restricted', + }, + resourceVersion: '470632', + name: 'test-deployment-1-54b47fbb75-jfxdt', + uid: '64ba1932-debd-11e9-b22b-06b197463f30', + creationTimestamp: '2019-09-24T11:21:03Z', + namespace: 'jeff-project', + ownerReferences: [ + { + apiVersion: 'apps/v1', + kind: 'ReplicaSet', + name: 'test-deployment-1-54b47fbb75', + uid: '64b4abd7-debd-11e9-b22b-06b197463f30', + controller: true, + blockOwnerDeletion: true, + }, + ], + labels: { app: 'hello-openshift', 'pod-template-hash': '54b47fbb75' }, + }, + // spec: { + // restartPolicy: 'Always', + // serviceAccountName: 'default', + // imagePullSecrets: [{ name: 'default-dockercfg-zch2x' }], + // priority: 0, + // schedulerName: 'default-scheduler', + // enableServiceLinks: true, + // terminationGracePeriodSeconds: 30, + // nodeName: 'ip-10-0-132-222.us-east-2.compute.internal', + // securityContext: { seLinuxOptions: { level: 's0:c23,c7' }, fsGroup: 1000520000 }, + // containers: [ + // { + // resources: {}, + // terminationMessagePath: '/dev/termination-log', + // name: 'hello-openshift', + // securityContext: { + // capabilities: { drop: ['KILL', 'MKNOD', 'SETGID', 'SETUID'] }, + // runAsUser: 1000520000, + // }, + // ports: [{ containerPort: 8080, protocol: 'TCP' }], + // imagePullPolicy: ImagePullPolicy.Always, + // volumeMounts: [ + // { + // name: 'default-token-5ffs4', + // readOnly: true, + // mountPath: '/var/run/secrets/kubernetes.io/serviceaccount', + // }, + // ], + // terminationMessagePolicy: 'File', + // image: 'openshift/hello-openshift', + // }, + // ], + // serviceAccount: 'default', + // volumes: [ + // { + // name: 'default-token-5ffs4', + // secret: { secretName: 'default-token-5ffs4', defaultMode: 420 }, + // }, + // ], + // dnsPolicy: 'ClusterFirst', + // tolerations: [ + // { + // key: 'node.kubernetes.io/not-ready', + // operator: 'Exists', + // effect: 'NoExecute', + // tolerationSeconds: 300, + // }, + // { + // key: 'node.kubernetes.io/unreachable', + // operator: 'Exists', + // effect: 'NoExecute', + // tolerationSeconds: 300, + // }, + // ], + // }, + // status: { + // phase: 'Running', + // conditions: [ + // { + // type: 'Initialized', + // status: 'True', + // lastProbeTime: null, + // lastTransitionTime: '2019-09-24T11:21:03Z', + // }, + // { + // type: 'Ready', + // status: 'True', + // lastProbeTime: null, + // lastTransitionTime: '2019-09-24T11:21:14Z', + // }, + // { + // type: 'ContainersReady', + // status: 'True', + // lastProbeTime: null, + // lastTransitionTime: '2019-09-24T11:21:14Z', + // }, + // { + // type: 'PodScheduled', + // status: 'True', + // lastProbeTime: null, + // lastTransitionTime: '2019-09-24T11:21:03Z', + // }, + // ], + // hostIP: '10.0.132.222', + // podIP: '10.129.2.19', + // startTime: '2019-09-24T11:21:03Z', + // containerStatuses: [ + // { + // name: 'hello-openshift', + // state: { running: { startedAt: '2019-09-24T11:21:13Z' } }, + // lastState: {}, + // ready: true, + // restartCount: 0, + // image: 'docker.io/openshift/hello-openshift:latest', + // imageID: + // 'docker.io/openshift/hello-openshift@sha256:aaea76ff622d2f8bcb32e538e7b3cd0ef6d291953f3e7c9f556c1ba5baf47e2e', + // containerID: + // 'cri-o://1b5cff087c7f6145d8fc422f1a474b869860eb8bfd68e3c5db85722d034aabf9', + // }, + // ], + // qosClass: 'BestEffort', + // }, + }, + { + apiVersion: 'v1', + kind: 'Pod', + metadata: { + generateName: 'test-deployment-1-54b47fbb75-', + annotations: { + 'k8s.v1.cni.cncf.io/networks-status': + '[{\n "name": "openshift-sdn",\n "interface": "eth0",\n "ips": [\n "10.131.0.20"\n ],\n "default": true,\n "dns": {}\n}]', + 'openshift.io/scc': 'restricted', + }, + resourceVersion: '470626', + name: 'test-deployment-1-54b47fbb75-7s8ct', + uid: '64b786cf-debd-11e9-b22b-06b197463f30', + creationTimestamp: '2019-09-24T11:21:03Z', + namespace: 'jeff-project', + ownerReferences: [ + { + apiVersion: 'apps/v1', + kind: 'ReplicaSet', + name: 'test-deployment-1-54b47fbb75', + uid: '64b4abd7-debd-11e9-b22b-06b197463f30', + controller: true, + blockOwnerDeletion: true, + }, + ], + labels: { app: 'hello-openshift', 'pod-template-hash': '54b47fbb75' }, + }, + // spec: { + // restartPolicy: 'Always', + // serviceAccountName: 'default', + // imagePullSecrets: [{ name: 'default-dockercfg-zch2x' }], + // priority: 0, + // schedulerName: 'default-scheduler', + // enableServiceLinks: true, + // terminationGracePeriodSeconds: 30, + // nodeName: 'ip-10-0-168-116.us-east-2.compute.internal', + // securityContext: { seLinuxOptions: { level: 's0:c23,c7' }, fsGroup: 1000520000 }, + // containers: [ + // { + // resources: {}, + // terminationMessagePath: '/dev/termination-log', + // name: 'hello-openshift', + // securityContext: { + // capabilities: { drop: ['KILL', 'MKNOD', 'SETGID', 'SETUID'] }, + // runAsUser: 1000520000, + // }, + // ports: [{ containerPort: 8080, protocol: 'TCP' }], + // imagePullPolicy: ImagePullPolicy.Always, + // volumeMounts: [ + // { + // name: 'default-token-5ffs4', + // readOnly: true, + // mountPath: '/var/run/secrets/kubernetes.io/serviceaccount', + // }, + // ], + // terminationMessagePolicy: 'File', + // image: 'openshift/hello-openshift', + // }, + // ], + // serviceAccount: 'default', + // volumes: [ + // { + // name: 'default-token-5ffs4', + // secret: { secretName: 'default-token-5ffs4', defaultMode: 420 }, + // }, + // ], + // dnsPolicy: 'ClusterFirst', + // tolerations: [ + // { + // key: 'node.kubernetes.io/not-ready', + // operator: 'Exists', + // effect: 'NoExecute', + // tolerationSeconds: 300, + // }, + // { + // key: 'node.kubernetes.io/unreachable', + // operator: 'Exists', + // effect: 'NoExecute', + // tolerationSeconds: 300, + // }, + // ], + // }, + // status: { + // phase: 'Running', + // conditions: [ + // { + // type: 'Initialized', + // status: 'True', + // lastProbeTime: null, + // lastTransitionTime: '2019-09-24T11:21:03Z', + // }, + // { + // type: 'Ready', + // status: 'True', + // lastProbeTime: null, + // lastTransitionTime: '2019-09-24T11:21:13Z', + // }, + // { + // type: 'ContainersReady', + // status: 'True', + // lastProbeTime: null, + // lastTransitionTime: '2019-09-24T11:21:13Z', + // }, + // { + // type: 'PodScheduled', + // status: 'True', + // lastProbeTime: null, + // lastTransitionTime: '2019-09-24T11:21:03Z', + // }, + // ], + // hostIP: '10.0.168.116', + // podIP: '10.131.0.20', + // startTime: '2019-09-24T11:21:03Z', + // containerStatuses: [ + // { + // name: 'hello-openshift', + // state: { running: { startedAt: '2019-09-24T11:21:13Z' } }, + // lastState: {}, + // ready: true, + // restartCount: 0, + // image: 'docker.io/openshift/hello-openshift:latest', + // imageID: + // 'docker.io/openshift/hello-openshift@sha256:aaea76ff622d2f8bcb32e538e7b3cd0ef6d291953f3e7c9f556c1ba5baf47e2e', + // containerID: + // 'cri-o://9286a37bb9880c398c31afb89f5a73ebc7e9361b3e2fe46d021d83df0bba3df0', + // }, + // ], + // qosClass: 'BestEffort', + // }, + }, + ], + }, + replicationControllers: { + loaded: true, + loadError: '', + data: [ + { + kind: 'replicationcontroller', + metadata: { + annotations: { + 'openshift.io/deployment-config.name': 'perl', + 'openshift.io/deployer-pod.completed-at': '2019-09-23 12:10:32 +0000 UTC', + 'openshift.io/deployment.phase': 'Failed', + 'openshift.io/deployer-pod.created-at': '2019-09-23 12:00:20 +0000 UTC', + 'openshift.io/deployment-config.latest-version': '1', + 'openshift.io/deployment.status-reason': 'config change', + 'kubectl.kubernetes.io/desired-replicas': '1', + 'openshift.io/deployment.replicas': '0', + 'openshift.io/encoded-deployment-config': + '{"kind":"DeploymentConfig","apiVersion":"apps.openshift.io/v1","metadata":{"name":"perl","namespace":"jeff-project","uid":"b69703c0-ddf9-11e9-b72f-0a580a810024","resourceVersion":"26857","generation":2,"creationTimestamp":"2019-09-23T12:00:19Z","labels":{"app":"perl","app.kubernetes.io/component":"perl","app.kubernetes.io/instance":"perl"},"annotations":{"openshift.io/generated-by":"OpenShiftWebConsole"}},"spec":{"strategy":{"type":"Rolling","rollingParams":{"updatePeriodSeconds":1,"intervalSeconds":1,"timeoutSeconds":600,"maxUnavailable":"25%","maxSurge":"25%"},"resources":{},"activeDeadlineSeconds":21600},"triggers":[{"type":"ImageChange","imageChangeParams":{"automatic":true,"containerNames":["perl"],"from":{"kind":"ImageStreamTag","namespace":"jeff-project","name":"perl:latest"},"lastTriggeredImage":"perl@sha256:711837fda379e492e351c0379ab697effc7e9c61dac2bef731073ac1138baad1"}},{"type":"ConfigChange"}],"replicas":1,"revisionHistoryLimit":10,"test":false,"selector":{"app":"perl","deploymentconfig":"perl"},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"perl","deploymentconfig":"perl"},"annotations":{"openshift.io/generated-by":"OpenShiftWebConsole"}},"spec":{"containers":[{"name":"perl","image":"perl@sha256:711837fda379e492e351c0379ab697effc7e9c61dac2bef731073ac1138baad1","ports":[{"containerPort":8080,"protocol":"TCP"}],"resources":{},"terminationMessagePath":"/dev/termination-log","terminationMessagePolicy":"File","imagePullPolicy":"IfNotPresent"}],"restartPolicy":"Always","terminationGracePeriodSeconds":30,"dnsPolicy":"ClusterFirst","securityContext":{},"schedulerName":"default-scheduler"}}},"status":{"latestVersion":1,"observedGeneration":1,"replicas":0,"updatedReplicas":0,"availableReplicas":0,"unavailableReplicas":0,"details":{"message":"config change","causes":[{"type":"ConfigChange"}]},"conditions":[{"type":"Available","status":"False","lastUpdateTime":"2019-09-23T12:00:19Z","lastTransitionTime":"2019-09-23T12:00:19Z","message":"Deployment config does not have minimum availability."}]}}\n', + 'openshift.io/deployer-pod.name': 'perl-1-deploy', + }, + resourceVersion: '29639', + name: 'perl-1', + uid: 'b74b9c2b-ddf9-11e9-8d63-02de4f087472', + creationTimestamp: '2019-09-23T12:00:20Z', + generation: 3, + namespace: 'jeff-project', + ownerReferences: [ + { + apiVersion: 'apps.openshift.io/v1', + kind: 'DeploymentConfig', + name: 'perl', + uid: 'b69703c0-ddf9-11e9-b72f-0a580a810024', + controller: true, + blockOwnerDeletion: true, + }, + ], + labels: { + app: 'perl', + 'app.kubernetes.io/component': 'perl', + 'app.kubernetes.io/instance': 'perl', + 'openshift.io/deployment-config.name': 'perl', + }, + }, + // spec: { + // replicas: 0, + // selector: { app: 'perl', deployment: 'perl-1', deploymentconfig: 'perl' }, + // template: { + // metadata: { + // creationTimestamp: null, + // labels: { app: 'perl', deployment: 'perl-1', deploymentconfig: 'perl' }, + // annotations: { + // 'openshift.io/deployment-config.latest-version': '1', + // 'openshift.io/deployment-config.name': 'perl', + // 'openshift.io/deployment.name': 'perl-1', + // 'openshift.io/generated-by': 'OpenShiftWebConsole', + // }, + // }, + // spec: { + // containers: [ + // { + // name: 'perl', + // image: + // 'perl@sha256:711837fda379e492e351c0379ab697effc7e9c61dac2bef731073ac1138baad1', + // ports: [{ containerPort: 8080, protocol: 'TCP' }], + // resources: {}, + // terminationMessagePath: '/dev/termination-log', + // terminationMessagePolicy: 'File', + // imagePullPolicy: ImagePullPolicy.IfNotPresent, + // }, + // ], + // restartPolicy: 'Always', + // terminationGracePeriodSeconds: 30, + // dnsPolicy: 'ClusterFirst', + // securityContext: {}, + // schedulerName: 'default-scheduler', + // }, + // }, + // }, + // status: { replicas: 0, observedGeneration: 3 }, + }, + ], + }, + routes: { + loaded: true, + loadError: '', + data: [ + { + kind: 'route', + metadata: { + name: 'cakephp-mysql-example', + namespace: 'jeff-project', + uid: '86d5f6f5-ddf4-11e9-a662-0a580a820020', + resourceVersion: '17026', + creationTimestamp: '2019-09-23T11:23:11Z', + labels: { + app: 'cakephp-mysql-example', + template: 'cakephp-mysql-example', + 'template.openshift.io/template-instance-owner': '86b6b79a-ddf4-11e9-a662-0a580a820020', + }, + annotations: { 'openshift.io/host.generated': 'true' }, + }, + // spec: { + // host: 'cakephp-mysql-example-jeff-project.apps.jsp002.devcluster.openshift.com', + // subdomain: '', + // to: { kind: 'Service', name: 'cakephp-mysql-example', weight: 100 }, + // wildcardPolicy: 'None', + // }, + // status: { + // ingress: [ + // { + // host: 'cakephp-mysql-example-jeff-project.apps.jsp002.devcluster.openshift.com', + // routerName: 'default', + // conditions: [ + // { type: 'Admitted', status: 'True', lastTransitionTime: '2019-09-23T11:23:11Z' }, + // ], + // wildcardPolicy: 'None', + // routerCanonicalHostname: 'apps.jsp002.devcluster.openshift.com', + // }, + // ], + // }, + }, + { + kind: 'route', + metadata: { + name: 'dotnet-example', + namespace: 'jeff-project', + uid: '7f419151-ddf4-11e9-a662-0a580a820020', + resourceVersion: '16945', + creationTimestamp: '2019-09-23T11:22:58Z', + labels: { + 'template.openshift.io/template-instance-owner': '7f25e822-ddf4-11e9-a662-0a580a820020', + }, + annotations: { 'openshift.io/host.generated': 'true' }, + }, + // spec: { + // host: 'dotnet-example-jeff-project.apps.jsp002.devcluster.openshift.com', + // subdomain: '', + // to: { kind: 'Service', name: 'dotnet-example', weight: 100 }, + // wildcardPolicy: 'None', + // }, + // status: { + // ingress: [ + // { + // host: 'dotnet-example-jeff-project.apps.jsp002.devcluster.openshift.com', + // routerName: 'default', + // conditions: [ + // { type: 'Admitted', status: 'True', lastTransitionTime: '2019-09-23T11:22:59Z' }, + // ], + // wildcardPolicy: 'None', + // routerCanonicalHostname: 'apps.jsp002.devcluster.openshift.com', + // }, + // ], + // }, + }, + { + kind: 'route', + metadata: { + name: 'perl', + namespace: 'jeff-project', + uid: 'b69e6da1-ddf9-11e9-b981-0a580a800015', + resourceVersion: '26846', + creationTimestamp: '2019-09-23T12:00:19Z', + labels: { + app: 'perl', + 'app.kubernetes.io/component': 'perl', + 'app.kubernetes.io/instance': 'perl', + }, + annotations: { + 'openshift.io/generated-by': 'OpenShiftWebConsole', + 'openshift.io/host.generated': 'true', + }, + }, + // spec: { + // host: 'perl-jeff-project.apps.jsp002.devcluster.openshift.com', + // subdomain: '', + // to: { kind: 'Service', name: 'perl', weight: 100 }, + // port: { targetPort: '8080-tcp' }, + // wildcardPolicy: 'None', + // }, + // status: { + // ingress: [ + // { + // host: 'perl-jeff-project.apps.jsp002.devcluster.openshift.com', + // routerName: 'default', + // conditions: [ + // { type: 'Admitted', status: 'True', lastTransitionTime: '2019-09-23T12:00:19Z' }, + // ], + // wildcardPolicy: 'None', + // routerCanonicalHostname: 'apps.jsp002.devcluster.openshift.com', + // }, + // ], + // }, + }, + ], + }, + services: { + loaded: true, + loadError: '', + data: [ + { + kind: 'service', + metadata: { + name: 'cakephp-mysql-example', + namespace: 'jeff-project', + uid: '86d4216e-ddf4-11e9-8d63-02de4f087472', + resourceVersion: '17014', + creationTimestamp: '2019-09-23T11:23:11Z', + labels: { + app: 'cakephp-mysql-example', + template: 'cakephp-mysql-example', + 'template.openshift.io/template-instance-owner': '86b6b79a-ddf4-11e9-a662-0a580a820020', + }, + annotations: { + description: 'Exposes and load balances the application pods', + 'service.alpha.openshift.io/dependencies': '[{"name": "mysql", "kind": "Service"}]', + }, + }, + // spec: { + // ports: [{ name: 'web', protocol: 'TCP', port: 8080, targetPort: 8080 }], + // selector: { name: 'cakephp-mysql-example' }, + // clusterIP: '172.30.245.103', + // type: 'ClusterIP', + // sessionAffinity: 'None', + // }, + // status: { loadBalancer: {} }, + }, + { + kind: 'service', + metadata: { + name: 'dotnet-example', + namespace: 'jeff-project', + uid: '7f438a59-ddf4-11e9-8d63-02de4f087472', + resourceVersion: '16937', + creationTimestamp: '2019-09-23T11:22:58Z', + labels: { + 'template.openshift.io/template-instance-owner': '7f25e822-ddf4-11e9-a662-0a580a820020', + }, + annotations: { description: 'Exposes and load balances the application pods' }, + }, + // spec: { + // ports: [{ name: 'web', protocol: 'TCP', port: 8080, targetPort: 8080 }], + // selector: { name: 'dotnet-example' }, + // clusterIP: '172.30.96.177', + // type: 'ClusterIP', + // sessionAffinity: 'None', + // }, + // status: { loadBalancer: {} }, + }, + { + kind: 'service', + metadata: { + name: 'mysql', + namespace: 'jeff-project', + uid: '86df9dbf-ddf4-11e9-8d63-02de4f087472', + resourceVersion: '17021', + creationTimestamp: '2019-09-23T11:23:11Z', + labels: { + app: 'cakephp-mysql-example', + template: 'cakephp-mysql-example', + 'template.openshift.io/template-instance-owner': '86b6b79a-ddf4-11e9-a662-0a580a820020', + }, + annotations: { description: 'Exposes the database server' }, + }, + // spec: { + // ports: [{ name: 'mysql', protocol: 'TCP', port: 3306, targetPort: 3306 }], + // selector: { name: 'mysql' }, + // clusterIP: '172.30.57.252', + // type: 'ClusterIP', + // sessionAffinity: 'None', + // }, + // status: { loadBalancer: {} }, + }, + { + kind: 'service', + metadata: { + name: 'perl', + namespace: 'jeff-project', + uid: 'b69e4877-ddf9-11e9-b012-0a0700ae5e38', + resourceVersion: '26842', + creationTimestamp: '2019-09-23T12:00:19Z', + labels: { + app: 'perl', + 'app.kubernetes.io/component': 'perl', + 'app.kubernetes.io/instance': 'perl', + }, + annotations: { 'openshift.io/generated-by': 'OpenShiftWebConsole' }, + }, + // spec: { + // ports: [{ name: '8080-tcp', protocol: 'TCP', port: 8080, targetPort: 8080 }], + // selector: { app: 'perl', deploymentconfig: 'perl' }, + // clusterIP: '172.30.7.197', + // type: 'ClusterIP', + // sessionAffinity: 'None', + // }, + // status: { loadBalancer: {} }, + }, + ], + }, + replicaSets: { + loaded: true, + loadError: '', + data: [ + { + kind: 'ReplicaSet', + apiVersion: 'apps/v1', + metadata: { + annotations: { + 'deployment.kubernetes.io/desired-replicas': '6', + 'deployment.kubernetes.io/max-replicas': '8', + 'deployment.kubernetes.io/revision': '1', + }, + resourceVersion: '471848', + name: 'test-deployment-1-54b47fbb75', + uid: '64b4abd7-debd-11e9-b22b-06b197463f30', + creationTimestamp: '2019-09-24T11:21:03Z', + generation: 4, + namespace: 'jeff-project', + ownerReferences: [ + { + apiVersion: 'apps/v1', + kind: 'Deployment', + name: 'test-deployment-1', + uid: '64b34874-debd-11e9-8cdf-0a0700ae5e38', + controller: true, + blockOwnerDeletion: true, + }, + ], + labels: { app: 'hello-openshift', 'pod-template-hash': '54b47fbb75' }, + }, + // spec: { + // replicas: 6, + // selector: { matchLabels: { app: 'hello-openshift', 'pod-template-hash': '54b47fbb75' } }, + // template: { + // metadata: { + // creationTimestamp: null, + // labels: { app: 'hello-openshift', 'pod-template-hash': '54b47fbb75' }, + // }, + // spec: { + // containers: [ + // { + // name: 'hello-openshift', + // image: 'openshift/hello-openshift', + // ports: [{ containerPort: 8080, protocol: 'TCP' }], + // resources: {}, + // terminationMessagePath: '/dev/termination-log', + // terminationMessagePolicy: 'File', + // imagePullPolicy: ImagePullPolicy.Always, + // }, + // ], + // restartPolicy: 'Always', + // terminationGracePeriodSeconds: 30, + // dnsPolicy: 'ClusterFirst', + // securityContext: {}, + // schedulerName: 'default-scheduler', + // }, + // }, + // }, + // status: { + // replicas: 6, + // fullyLabeledReplicas: 6, + // readyReplicas: 6, + // availableReplicas: 6, + // observedGeneration: 4, + // }, + }, + ], + }, + buildConfigs: { + loaded: true, + loadError: '', + data: [ + { + kind: 'buildconfig', + metadata: { + name: 'cakephp-mysql-example', + namespace: 'jeff-project', + uid: '86da21d7-ddf4-11e9-a662-0a580a820020', + resourceVersion: '17018', + creationTimestamp: '2019-09-23T11:23:11Z', + labels: { + app: 'cakephp-mysql-example', + template: 'cakephp-mysql-example', + 'template.openshift.io/template-instance-owner': '86b6b79a-ddf4-11e9-a662-0a580a820020', + }, + annotations: { + description: 'Defines how to build the application', + 'template.alpha.openshift.io/wait-for-ready': 'true', + }, + }, + // spec: { + // nodeSelector: null, + // output: { to: { kind: 'ImageStreamTag', name: 'cakephp-mysql-example:latest' } }, + // resources: {}, + // successfulBuildsHistoryLimit: 5, + // failedBuildsHistoryLimit: 5, + // strategy: { + // type: 'Source', + // sourceStrategy: { + // from: { kind: 'ImageStreamTag', namespace: 'openshift', name: 'php:7.1' }, + // env: [{ name: 'COMPOSER_MIRROR' }], + // }, + // }, + // postCommit: { script: './vendor/bin/phpunit' }, + // source: { type: 'Git', git: { uri: 'https://github.com/sclorg/cakephp-ex.git' } }, + // triggers: [ + // { type: 'ImageChange', imageChange: {} }, + // { type: 'ConfigChange' }, + // { type: 'GitHub', github: { secret: 'i2FDtC6Ba1FhveTVC8FdnpULyQuIlvxyOwex0n4q' } }, + // ], + // runPolicy: 'Serial', + // }, + // status: { lastVersion: 0 }, + }, + { + kind: 'buildconfig', + metadata: { + name: 'dotnet-example', + namespace: 'jeff-project', + uid: '7f4a56aa-ddf4-11e9-b981-0a580a800015', + resourceVersion: '16940', + creationTimestamp: '2019-09-23T11:22:58Z', + labels: { + 'template.openshift.io/template-instance-owner': '7f25e822-ddf4-11e9-a662-0a580a820020', + }, + annotations: { description: 'Defines how to build the application' }, + }, + // spec: { + // nodeSelector: null, + // output: { to: { kind: 'ImageStreamTag', name: 'dotnet-example:latest' } }, + // resources: {}, + // successfulBuildsHistoryLimit: 5, + // failedBuildsHistoryLimit: 5, + // strategy: { + // type: 'Source', + // sourceStrategy: { + // from: { kind: 'ImageStreamTag', namespace: 'openshift', name: 'dotnet:2.2' }, + // env: [ + // { name: 'DOTNET_STARTUP_PROJECT', value: 'app' }, + // { name: 'DOTNET_ASSEMBLY_NAME' }, + // { name: 'DOTNET_NPM_TOOLS' }, + // { name: 'DOTNET_TEST_PROJECTS' }, + // { name: 'DOTNET_CONFIGURATION', value: 'Release' }, + // { name: 'DOTNET_RESTORE_SOURCES' }, + // { name: 'DOTNET_TOOLS' }, + // ], + // }, + // }, + // postCommit: {}, + // source: { + // type: 'Git', + // git: { + // uri: 'https://github.com/redhat-developer/s2i-dotnetcore-ex.git', + // ref: 'dotnetcore-2.2', + // }, + // }, + // triggers: [ + // { type: 'ImageChange', imageChange: {} }, + // { type: 'ConfigChange' }, + // { type: 'GitHub', github: { secret: '23NXE12vg3X1qQSaXR17tLduAFAXUWHhNdSW3QkI' } }, + // { type: 'Generic', generic: { secret: '6CYilKtmyOHqKMPgOVh1XUUd2Q8HNxYehFD51Hk5' } }, + // ], + // runPolicy: 'Serial', + // }, + // status: { lastVersion: 0 }, + }, + ], + }, + builds: { loaded: true, loadError: '', data: [] }, + statefulSets: { + loaded: true, + loadError: '', + data: [ + { + kind: 'statefulset', + metadata: { + name: 'test-statefulset-1', + namespace: 'jeff-project', + uid: '75c049b5-debd-11e9-8cdf-0a0700ae5e38', + resourceVersion: '471258', + generation: 1, + creationTimestamp: '2019-09-24T11:21:31Z', + labels: { 'app.kubernetes.io/part-of': 'application-3' }, + }, + // spec: { + // replicas: 3, + // selector: { matchLabels: { app: 'httpd' } }, + // template: { + // metadata: { creationTimestamp: null, labels: { app: 'httpd' } }, + // spec: { + // containers: [ + // { + // name: 'httpd', + // image: 'image-registry.openshift-image-registry.svc:5000/openshift/httpd:latest', + // ports: [{ name: 'web', containerPort: 8080, protocol: 'TCP' }], + // resources: {}, + // volumeMounts: [{ name: 'www', mountPath: '/var/www/html' }], + // terminationMessagePath: '/dev/termination-log', + // terminationMessagePolicy: 'File', + // imagePullPolicy: ImagePullPolicy.IfNotPresent, + // }, + // ], + // restartPolicy: 'Always', + // terminationGracePeriodSeconds: 10, + // dnsPolicy: 'ClusterFirst', + // securityContext: {}, + // schedulerName: 'default-scheduler', + // }, + // }, + // volumeClaimTemplates: [ + // { + // metadata: { name: 'www', creationTimestamp: null }, + // spec: { + // accessModes: ['ReadWriteOnce'], + // resources: { requests: { storage: '1Gi' } }, + // storageClassName: 'my-storage-class', + // volumeMode: 'Filesystem', + // }, + // status: { phase: 'Pending' }, + // }, + // ], + // serviceName: 'httpd', + // podManagementPolicy: 'OrderedReady', + // updateStrategy: { type: 'RollingUpdate', rollingUpdate: { partition: 0 } }, + // revisionHistoryLimit: 10, + // }, + // status: { + // observedGeneration: 1, + // replicas: 1, + // currentReplicas: 1, + // updatedReplicas: 1, + // currentRevision: 'test-statefulset-1-7d57df7bfc', + // updateRevision: 'test-statefulset-1-7d57df7bfc', + // collisionCount: 0, + // }, + }, + ], + }, +}; diff --git a/src/__tests__/service-binding-test-data.ts b/src/__tests__/service-binding-test-data.ts new file mode 100644 index 0000000..824487d --- /dev/null +++ b/src/__tests__/service-binding-test-data.ts @@ -0,0 +1,301 @@ +import { getAPIVersionForModel, K8sResourceCommon } from '@openshift-console/dynamic-plugin-sdk'; +import { TopologyDataResources } from '@openshift-console/dynamic-plugin-sdk/lib/extensions/topology-types'; + +import { ServiceBindingModel } from '../models/ServiceBindingModel'; +import { DeploymentKind } from '../utils/types/commonTypes'; + +export const sbrBackingServiceSelectors: Partial = { + deployments: { + loaded: true, + loadError: null, + data: [ + { + apiVersion: 'apps/v1', + kind: 'Deployment', + metadata: { + name: 'app', + uid: 'uid-app', + }, + } as DeploymentKind, + { + apiVersion: 'apps/v1', + kind: 'Deployment', + metadata: { + name: 'db-1', + uid: 'uid-db-1', + ownerReferences: [ + { + apiVersion: 'db/v1alpha1', + kind: 'Database', + name: 'db-demo1', + uid: 'uid-db-demo1', + }, + ], + }, + } as DeploymentKind, + { + apiVersion: 'apps/v1', + kind: 'Deployment', + metadata: { + name: 'db-2', + uid: 'uid-db-2', + ownerReferences: [ + { + apiVersion: 'postgresql.baiju.dev/v1alpha1', + kind: 'Database', + name: 'db-demo2', + uid: 'uid-db-demo2', + }, + ], + }, + } as DeploymentKind, + ], + }, + serviceBindingRequests: { + loaded: true, + loadError: null, + data: [ + { + apiVersion: getAPIVersionForModel(ServiceBindingModel), + kind: ServiceBindingModel.kind, + metadata: { + name: 'sbr-1', + }, + // spec: { + // application: { + // name: 'app', + // group: 'apps', + // version: 'v1', + // resource: 'deployments', + // }, + // services: [ + // { + // group: 'postgresql.baiju.dev', + // version: 'v1alpha1', + // kind: 'Jaeger', + // name: 'jaeger-all-in-one-inmemory', + // }, + // { + // group: 'postgresql.baiju.dev', + // version: 'v1alpha1', + // kind: 'Jaeger', + // name: 'jaeger-all-in-one-inmemory', + // }, + // ], + // detectBindingResources: true, + // }, + }, + ], + }, +}; + +export const sbrBackingServiceSelector: Partial = { + deployments: { + loaded: true, + loadError: null, + data: [ + { + apiVersion: 'apps/v1', + kind: 'Deployment', + metadata: { + name: 'app', + uid: 'uid-app', + }, + } as DeploymentKind, + { + apiVersion: 'apps/v1', + kind: 'Deployment', + metadata: { + name: 'db-1', + uid: 'uid-db-1', + ownerReferences: [ + { + apiVersion: 'db/v1alpha1', + kind: 'Database', + name: 'db-demo1', + uid: 'uid-db-demo1', + }, + ], + }, + } as DeploymentKind, + ], + }, + serviceBindingRequests: { + loaded: true, + loadError: null, + data: [ + { + apiVersion: getAPIVersionForModel(ServiceBindingModel), + kind: ServiceBindingModel.kind, + metadata: { + name: 'sbr-2', + }, + // spec: { + // application: { + // name: 'app', + // group: 'apps', + // version: 'v1', + // resource: 'deployments', + // }, + // services: [ + // { + // group: 'postgresql.baiju.dev', + // version: 'v1alpha1', + // kind: 'Jaeger', + // name: 'jaeger-all-in-one-inmemory', + // }, + // ], + // detectBindingResources: true, + // }, + }, + ], + }, +}; + +const deploymentWithLabels: K8sResourceCommon = { + apiVersion: 'apps/v1', + kind: 'Deployment', + metadata: { + name: 'app', + uid: 'uid-app', + labels: { app: 'app' }, + }, +}; + +export const sbrLabelSelectorBackingServiceSelector: Partial = { + deployments: { + loaded: true, + loadError: null, + data: [ + deploymentWithLabels as DeploymentKind, + { + apiVersion: 'apps/v1', + kind: 'Deployment', + metadata: { + name: 'db-1', + uid: 'uid-db-1', + ownerReferences: [ + { + apiVersion: 'db/v1alpha1', + kind: 'Database', + name: 'db-demo1', + uid: 'uid-db-demo1', + }, + ], + }, + } as DeploymentKind, + ], + }, + serviceBindingRequests: { + loaded: true, + loadError: null, + data: [ + { + apiVersion: getAPIVersionForModel(ServiceBindingModel), + kind: ServiceBindingModel.kind, + metadata: { + name: 'sbr-2', + }, + // spec: { + // application: { + // labelSelector: { + // matchLabels: { + // app: 'app', + // }, + // }, + // group: 'apps', + // version: 'v1', + // resource: 'deployments', + // }, + // services: [ + // { + // group: 'postgresql.baiju.dev', + // version: 'v1alpha1', + // kind: 'Jaeger', + // name: 'jaeger-all-in-one-inmemory', + // }, + // ], + // detectBindingResources: true, + // }, + }, + ], + }, +}; + +export const sbrLabelSelectorBackingServiceSelectors: Partial = { + deployments: { + loaded: true, + loadError: null, + data: [ + deploymentWithLabels as DeploymentKind, + { + apiVersion: 'apps/v1', + kind: 'Deployment', + metadata: { + name: 'db-1', + uid: 'uid-db-1', + ownerReferences: [ + { + apiVersion: 'db/v1alpha1', + kind: 'Database', + name: 'db-demo1', + uid: 'uid-db-demo1', + }, + ], + }, + } as DeploymentKind, + { + apiVersion: 'apps/v1', + kind: 'Deployment', + metadata: { + name: 'db-2', + uid: 'uid-db-2', + ownerReferences: [ + { + apiVersion: 'postgresql.baiju.dev/v1alpha1', + kind: 'Database', + name: 'db-demo2', + uid: 'uid-db-demo2', + }, + ], + }, + } as DeploymentKind, + ], + }, + serviceBindingRequests: { + loaded: true, + loadError: null, + data: [ + { + apiVersion: getAPIVersionForModel(ServiceBindingModel), + kind: ServiceBindingModel.kind, + metadata: { + name: 'sbr-1', + }, + // spec: { + // application: { + // name: 'app', + // group: 'apps', + // version: 'v1', + // resource: 'deployments', + // }, + // services: [ + // { + // group: 'postgresql.baiju.dev', + // version: 'v1alpha1', + // kind: 'Jaeger', + // name: 'jaeger-all-in-one-inmemory', + // }, + // { + // group: 'postgresql.baiju.dev', + // version: 'v1alpha1', + // kind: 'Jaeger', + // name: 'jaeger-all-in-one-inmemory', + // }, + // ], + // detectBindingResources: true, + // }, + }, + ], + }, +}; diff --git a/src/__tests__/topology-test-data.ts b/src/__tests__/topology-test-data.ts new file mode 100644 index 0000000..8f0ece5 --- /dev/null +++ b/src/__tests__/topology-test-data.ts @@ -0,0 +1,306 @@ +import { FirehoseResult } from '@openshift-console/dynamic-plugin-sdk'; +import { EventKind } from '@openshift-console/dynamic-plugin-sdk/lib/api/internal-types'; +import { TopologyDataResources } from '@openshift-console/dynamic-plugin-sdk/lib/extensions/topology-types'; +import { Model } from '@patternfly/react-topology'; + +import { NODE_HEIGHT, NODE_PADDING, NODE_WIDTH } from '../const'; +import { WorkloadModelProps } from '../data-transforms/transform-utils'; +import { sampleDeployments } from '../utils/__tests__/test-resource-data'; +import { CamelKameletBindingModel, KafkaSinkModel } from '../utils/knative-models'; +import { OdcNodeModel } from '../utils/types/topology-types'; + +export const TEST_KINDS_MAP = { + deploymentConfigs: 'DeploymentConfig', + deployments: 'Deployment', + daemonSets: 'DaemonSet', + pods: 'Pod', + replicationControllers: 'ReplicationController', + routes: 'Route', + services: 'Service', + replicaSets: 'ReplicaSet', + buildConfigs: 'BuildConfig', + builds: 'Build', + statefulSets: 'StatefulSet', + secrets: 'Secret', + clusterServiceVersions: 'operators.coreos.com~v1alpha1~ClusterServiceVersion', + serviceBindingRequests: 'operators.coreos.com~v1alpha1~ServiceBinding', + revisions: 'serving.knative.dev~v1~Revision', + configurations: 'serving.knative.dev~v1~Configuration', + ksroutes: 'serving.knative.dev~v1~Route', + ksservices: 'serving.knative.dev~v1~Service', + 'sources.knative.dev~v1alpha1~ApiServerSource': 'sources.knative.dev~v1alpha1~ApiServerSource', + 'sources.eventing.knative.dev~v1alpha1~ContainerSource': + 'sources.eventing.knative.dev~v1alpha1~ContainerSource', + 'sources.knative.dev~v1alpha1~KafkaSource': 'sources.knative.dev~v1alpha1~KafkaSource', + 'sources.knative.dev~v1alpha1~PingSource': 'sources.knative.dev~v1alpha1~PingSource', + 'sources.knative.dev~v1alpha1~SinkBinding': 'sources.knative.dev~v1alpha1~SinkBinding', + virtualmachines: 'VirtualMachine', + virtualmachineinstances: 'VirtualMachineInstance', + virtualmachinetemplates: 'Template', + migrations: 'VirtualMachineInstanceMigration', + dataVolumes: 'DataVolume', + vmImports: 'VirtualMachineImport', + brokers: 'Broker', + triggers: 'Trigger', + [CamelKameletBindingModel.plural]: CamelKameletBindingModel.kind, + [KafkaSinkModel.kind]: KafkaSinkModel.kind, +}; + +export const resources: TopologyDataResources = { + replicationControllers: { loaded: true, loadError: '', data: [] }, + pods: { loaded: true, loadError: '', data: [] }, + deploymentConfigs: { loaded: true, loadError: '', data: [] }, + services: { loaded: true, loadError: '', data: [] }, + routes: { loaded: true, loadError: '', data: [] }, + deployments: { loaded: true, loadError: '', data: [] }, + replicaSets: { loaded: true, loadError: '', data: [] }, + buildConfigs: { loaded: true, loadError: '', data: [] }, + builds: { loaded: true, loadError: '', data: [] }, + daemonSets: { loaded: true, loadError: '', data: [] }, + statefulSets: { loaded: true, loadError: '', data: [] }, + events: { loaded: true, loadError: '', data: [] }, +}; + +export const topologyData: Model = { + nodes: [], + edges: [], +}; + +const topologyDataModelNodes: OdcNodeModel[] = [ + { + id: 'e187afa2-53b1-406d-a619-cf9ff1468031', + type: 'workload', + label: 'hello-openshift', + resource: sampleDeployments.data[0], + data: { + data: {}, + id: 'e187afa2-53b1-406d-a619-cf9ff1468031', + name: 'hello-openshift', + type: 'workload', + resources: { + buildConfigs: [], + obj: sampleDeployments.data[0], + routes: [], + services: [], + }, + }, + ...WorkloadModelProps, + }, + { + id: 'e187afa2-53b1-406d-a619-cf9ff1468032', + type: 'workload', + label: 'hello-openshift-1', + resource: sampleDeployments.data[1], + data: { + data: {}, + id: 'e187afa2-53b1-406d-a619-cf9ff1468032', + name: 'hello-openshift-1', + type: 'workload', + resources: { + buildConfigs: [], + obj: sampleDeployments.data[1], + routes: [], + services: [], + }, + }, + ...WorkloadModelProps, + }, +]; + +export const topologyDataModel: Model = { + nodes: topologyDataModelNodes, + edges: [], +}; + +const dataModelNodes: OdcNodeModel[] = [ + { + data: topologyDataModel.nodes[0].data, + resource: topologyDataModelNodes[0].resource, + id: 'e187afa2-53b1-406d-a619-cf9ff1468031', + label: 'hello-openshift', + type: 'workload', + visible: true, + group: false, + width: NODE_WIDTH, + height: NODE_HEIGHT, + style: { + padding: NODE_PADDING, + }, + }, + { + data: topologyDataModel.nodes[1].data, + resource: topologyDataModelNodes[1].resource, + id: 'e187afa2-53b1-406d-a619-cf9ff1468032', + label: 'hello-openshift-1', + type: 'workload', + visible: true, + group: false, + width: NODE_WIDTH, + height: NODE_HEIGHT, + style: { + padding: NODE_PADDING, + }, + }, +]; + +export const dataModel: Model = { + nodes: dataModelNodes, + edges: [], +}; + +export const sampleHelmChartDeploymentConfig = { + kind: 'DeploymentConfig', + apiVersion: 'apps/v1', + metadata: { + name: 'nodejs-helm', + namespace: 'testproject1', + uid: 'b69ey0df-3f9382-11e9-02f68-525400680f2', + resourceVersion: '732186', + generation: 2, + creationTimestamp: '2019-04-22T11:58:33Z', + labels: { + app: 'nodejs-helm', + heritage: 'Helm', + chart: 'Nodejs', + release: 'nodejs-helm-12345', + }, + annotations: { + 'app.openshift.io/vcs-uri': 'https://github.com/redhat-developer/topology-example', + 'app.openshift.io/vcs-ref': 'master', + }, + }, + spec: { + strategy: { + type: 'Rolling', + }, + template: { + metadata: { + creationTimestamp: null, + labels: { + app: 'nodejs-helm', + deploymentconfig: 'nodejs-helm', + }, + }, + spec: {}, + }, + }, + status: { + availableReplicas: 1, + unavailableReplicas: 0, + latestVersion: 1, + updatedReplicas: 1, + replicas: 1, + readyReplicas: 1, + }, +}; + +export const sampleHelmResourcesMap = { + 'DeploymentConfig---nodejs-helm': { + releaseName: 'nodejs-helm', + releaseVersion: 1, + chartIcon: '', + manifestResources: [sampleHelmChartDeploymentConfig], + releaseNotes: 'test release notes', + status: 'deployed', + }, +}; + +export const sampleEventsResource: FirehoseResult = { + loaded: true, + loadError: '', + data: [ + { + apiVersion: 'v1', + kind: 'Event', + type: 'Normal', + lastTimestamp: '2020-01-23T10:00:47Z', + reason: 'Started', + firstTimestamp: '2020-01-23T08:21:06Z', + involvedObject: { + kind: 'Pod', + namespace: 'testproject3', + name: 'analytics-deployment-59dd7c47d4-2jp7t', + uid: 'f5ee90e4-959f-47df-b305-56a78cb047ea', + }, + source: { + component: 'kubelet', + host: 'ip-10-0-130-190.us-east-2.compute.internal', + }, + }, + ], +}; + +export const MockKialiGraphData = { + nodes: [ + { + data: { + id: '5cd385c1ee3309ae40828b5702ae57fb', + nodeType: 'workload', + namespace: 'testproject1', + workload: 'wit-deployment', + app: 'details', + version: 'v1', + destServices: [ + { + namespace: 'bookinfo', + name: 'details', + }, + ], + traffic: [ + { + protocol: 'http', + rates: { + httpIn: '0.04', + }, + }, + ], + }, + }, + { + data: { + id: '240c2314cefc993c5d9479a5c349fbd2', + nodeType: 'workload', + namespace: 'testproject1', + workload: 'analytics-deployment', + app: 'productpage', + version: 'v1', + destServices: [ + { + namespace: 'bookinfo', + name: 'productpage', + }, + ], + traffic: [ + { + protocol: 'http', + rates: { + httpIn: '0.04', + httpOut: '0.08', + }, + }, + ], + }, + }, + ], + edges: [ + { + data: { + id: 'df66cffc756bf9983dd453837e4e14a7', + source: '240c2314cefc993c5d9479a5c349fbd2', + target: '5cd385c1ee3309ae40828b5702ae57fb', + traffic: { + protocol: 'http', + rates: { + http: '0.04', + httpPercentReq: '50.6', + }, + responses: { + '200': { + '-': '100.0', + }, + }, + }, + }, + }, + ], +}; diff --git a/src/__tests__/topology-utils.spec.ts b/src/__tests__/topology-utils.spec.ts new file mode 100644 index 0000000..262b5ba --- /dev/null +++ b/src/__tests__/topology-utils.spec.ts @@ -0,0 +1,53 @@ +import { k8sPatch } from '@openshift-console/dynamic-plugin-sdk'; + +import { createTopologyResourceConnection, getTopologyResourceObject } from '../utils'; +import { sampleDeployments } from '../utils/__tests__/test-resource-data'; +import { OdcNodeModel } from '../utils/types/topology-types'; + +import { topologyDataModel } from './topology-test-data'; + +let patchData = null; + +describe('Topology Utils', () => { + beforeAll(() => { + jest.fn(k8sPatch).mockImplementation(({ model: model, resource: item, data: [patch] }) => { + patchData = patch; + return Promise.resolve(); + }); + }); + + afterAll(() => { + jest.restoreAllMocks(); + }); + + it('should create topology visual connector', async (done) => { + const source = (topologyDataModel.nodes[0] as OdcNodeModel).resource; + const target = (topologyDataModel.nodes[1] as OdcNodeModel).resource; + await createTopologyResourceConnection(source, target, null).catch(() => { + // Expected, network request failure for update + }); + const expectedConnectsToValue = [ + target.metadata.labels['app.kubernetes.io/instance'], + { + apiVersion: target.apiVersion, + kind: target.kind, + name: target.metadata.name, + }, + ]; + const expectedPatchData = { + op: 'replace', + path: '/metadata/annotations', + value: { + 'app.openshift.io/connects-to': JSON.stringify(expectedConnectsToValue), + }, + }; + + expect(patchData[0]).toEqual(expectedPatchData); + done(); + }); + + it('should return topology resource object', () => { + const topologyResourceObject = getTopologyResourceObject(topologyDataModel.nodes[0].data); + expect(topologyResourceObject).toEqual(sampleDeployments.data[0]); + }); +}); diff --git a/src/actions/TopologyActions.tsx b/src/actions/TopologyActions.tsx new file mode 100644 index 0000000..f85395d --- /dev/null +++ b/src/actions/TopologyActions.tsx @@ -0,0 +1,39 @@ +import React, { FC, useMemo } from 'react'; + +import { ActionServiceProvider } from '@openshift-console/dynamic-plugin-sdk'; +import { ActionMenuVariant } from '@openshift-console/dynamic-plugin-sdk/lib/api/internal-types'; +import { GraphElement, observer } from '@patternfly/react-topology'; + +import ActionMenu from '../components/ActionMenu/ActionMenu'; +import { getResource } from '../utils'; +import { getReferenceForResource } from '../utils/k8s-utils'; + +type TopologyActionsProps = { + element: GraphElement; +}; + +const TopologyActions: FC = ({ element }) => { + const resource = getResource(element); + const context = useMemo(() => { + const { csvName } = element.getData()?.data ?? {}; + return { + 'topology-actions': element, + 'topology-context-actions': { element }, + ...(resource ? { [getReferenceForResource(resource)]: resource } : {}), + ...(csvName ? { 'csv-actions': { csvName, resource } } : {}), + }; + }, [element, resource]); + return ( + + {({ actions, options, loaded }) => { + return ( + loaded && ( + + ) + ); + }} + + ); +}; + +export default observer(TopologyActions); diff --git a/src/actions/__tests__/contextMenuActions.spec.ts b/src/actions/__tests__/contextMenuActions.spec.ts new file mode 100644 index 0000000..79ac6ba --- /dev/null +++ b/src/actions/__tests__/contextMenuActions.spec.ts @@ -0,0 +1,54 @@ +import { OdcBaseNode } from '../../elements'; +import { TYPE_OPERATOR_BACKED_SERVICE } from '../../operators/components/const'; +import { getReferenceForResource } from '../../utils/k8s-utils'; +import { contextMenuActions } from '../contextMenuActions'; + +describe('context menu actions', () => { + it('should return proper context data for Export operator backed service', () => { + const mockOperatorBackedServiceNode = new OdcBaseNode(); + const OperatorBackedServiceModel = { + id: 'mock-operator-backed-service-id', + type: TYPE_OPERATOR_BACKED_SERVICE, + label: '', + resource: { + kind: 'Export', + apiVersion: 'primer.gitops.io/v1alpha1', + metadata: { + annotations: { + 'olm.operatorGroup': 'gitops-primer-system-mjlrj', + 'olm.operatorNamespace': 'gitops-primer-system', + }, + labels: { + 'olm.copiedFrom': 'gitops-primer-system', + }, + name: 'primer', + namespace: 'test-ns', + uid: 'edcfba4d-2e38-4337-b3fe-beb93b96145b', + }, + }, + data: { + resources: {}, + groupResources: [], + data: { + apiVersion: 'primer.gitops.io/v1alpha1', + builderImage: + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAIAAAAiOjnJAAATfUlEQVR4nOzdeVgT194H8EkCBEjYA+JVEBRQtKKAXhGCAioutaAttd6ub9Xa7RHfWpfaXrWbbRWXR3tRKy16sbe12l7bioCK7FJxqQKKbEILyE5kCWEJSd5H8z5xcmYyGZBDGvl9Hv+AwyQ5HL6Zc+acMcfEf9pHBACDjWvoCoDHEwQLYAHBAlhAsAAWECyABQQLYAHBAlhAsAAWECyABQQLYAHBAlhAsAAWECyABQQLYAHBAlhAsAAWECyABQQLYAHBAlhAsAAWECyABQQLYAHBAlhAsAAWECyABQQLYAHBAlhAsAAWECyABQQLYAHBAlhAsAAWECyABQQLYAHBAlhAsAAWECyABQQLYAHBAlhAsAAWECyABQQLYAHBAlhAsAAWECyABQQLYAHBAlhAsAAWECyABQQLYAHBAlhAsAAWECyABQQLYAHBAlhAsAAWJoauwNBxspePcuwx1KvLunmlVeYqFcdQFRhiwyhYy8ObBBZKA1bgxDnHO3f5BqzAUBpGXSHfTGXYCpiZKQxbgaE0jIIFhhIEC2ABwQJYDKPBe78UVzT8frNKqWIalo1wsA4L9OJx4c1JA4JFo6W186N9Z+R9+sfaXT29EXN8hqRSRgbebTQ6OrvZpIogCEmrDH91jBKcsWi4jXKIWuiX+/sdhYJp3mu0s13EXDhd0YNg0Vu+2H/5Yn9D18KIQVcIsIBgASwgWAALCBbAAoIFsIBgASwgWAALCBbAAoIFsIBgASwgWAALCBbAAoIFsIBgASwgWAALCBbAAoIFsIBgASwgWAALCBbAAoIFsIBgASwgWAALCBbAAoIFsIBgASwgWAALCBbAAoIFsIBgASwgWAALCNbQGS6bUjwwjD547XaFhadrt6FeXdbNq20aLttSDK9gJebYG7oKwwh0hQAL4zhjWVkqeDzD7ISjUHA6ZDxyCd9MacE32GZPHTKeQmEEozUjCFbA5I7QaW0GrEDOdevsG9bqrx1s+lZGNhgq5QRBNLeaxp0aYahXZ88IukJH2z4DV8BOrvnazqrPgKkiCEJkK+cYwQnLGIIFjBEEC2ABwQJYGMHgvV9UKqKiuqlT1stwjKWF2ThXx6EZqTRLpLWNTFcePB53rIuDhbnZUNRmCD1uwfr6xMWzWUV6Dwud6fX2i7NxV6ag+O72AynMG/LcH4/bCXa//4zA8rGal3/cusJrhX+yOexqYRX+uhA3imr0pur+We1eZ2VNyxDUZyg9bsGaK/Zm08eFi72HoDKB/mMtLfT3cW6jHTzdnIagPkPpcesKoxb4hou9u7rlDMeY801srCyGoDIeYxzjPnuhtb2L4Rgul+NgK+ByjWFuqj8et2ARBGEtNLcWmhu6Fv+Pb2YyQmRl6FoYwOPWFYK/CAgWwAKCBbCAYAEsIFgACwgWwAKCBbCAYAEsIFgACwgWwAKCBbCAYAEsIFgACwgWwAKCBbCAYAEsIFgACwgWwAKCBbCAYAEsIFgACwgWwAKCBbCAYAEsIFgACwgWwAKCBbCAYAEsIFgACwgWwAKCBbAwgmC1dfIMW4FW6cMKdHTyVIbcP4Bolxq4Aixx/Kd9ZOg66MHhqJxs+zgG2g9CqeQ03zNVkl7cRthnYW6wvXRaO0y6e4zgdGAEn+inUnEa7pkauhYPtUlN2qSGrsRfnhFkHxgjCBbAAoIFsDDYGIvH406d6urm5iASWQmFfElLZ2NTe2Hh3aqqx+2j9PGhbcObN+/++afh29AAwXJ3F73yctCsWeOtrWk+NLuisiklufD743ldXUyf1T7MubmJXnklaPYsL2trmg+sr6xsTk4pPH48T8a4pxBWbKcbrKzMP3h/MblEqVS9/8FPyGFPPukTLPZCCv+dcPH27TqCIIRCfnT0vMiIqTyeni64uVkaG3vhdGI+9Udvvx3mMvrhtuGxB9KqqyXUw0aMsH7nf8PJJR3S7u3bEydNGvXSizORgz/59HRnZw/1ScLDJ4WFau1h0dTcsXv32SlTXP6xfAbzr0C2MyZ5wfwnfHxcyIWtrbIvdiRRD54+3f2Zp/2Rwv1fptbWthIEIRDw10bPjYz01duGLS3S2Ni0X0/fQMo5HOLzz6IYHtjdLa+rb8vOLi0qqtX3m+nE9oxlZmYyd+5EcklfHzqX4+Pj8s8PnjI11ZrPTEoqUKdq9Gi7PXuWj3V3ZPNyIpFw27ZInykuO3cmy+UK8o/+Pt190qRRmm89PJxefuVr6ltTIOAjFW5pkW6//25uCgz0sLTU2onkfGrRhQs0Wzste3b61Kmu5JKEhFyCIJydbZAnZxZ7IO1WUe26dfOR8qTkwoKCaqRwxavi6dPdySX5+dXqVI0aZbd3z/KxY1m1oYODcOvWCB8flx07k8htyOFw2FT+tVWzrl+v+vzzMxWVTWxeDjFog3eRSLjjiygkVcXFdds/SyQIwtXF/uiRlSxTpbF0id/OHc8ybwfi5ib68MNI9nvEyWS9GZklSGGw2JN6pLW1xeTJo5HCpOQCtq+kLT+/Oi+vAimMipqGlIx1d5w2zR0pPHAwnSAIF3UbskuVxpIlvrtilg1sSxVfX9f4+BW+vq4DeOzgBMvMzGT3ruccHbX29mhpka5794eenj6BgL9r13O2tpYDeObgYK81a+YyHxMW6v388wHsnzOR0sMGBXlSmz4wcBzS3RQX15WXN7J/IURsbBqyGjNv7kR7ewG5ZNmy6cibJC+v4tq1PwQC/u5dz9nZDaQNg4I810bPG1idhUL+rpjnRo606e8DB2fwvmnjQnL3RBCEXK54b/OPjY3t6p/Svs9aW2WZmSXl5Y2yrl4He4Gf35hp09xNTNCsv/TizLy8ikuX7jBUYG30vDt3mpiP0bh6tbK+vs3Z+WFj2dlZPjFpVEFhDfkwcRA6WExKGuDpSq3odm1ubllQ0MOzo6kpLyJi6tGjF9XfWlqaLVw4GXnUoa8yCILYsH6BrjbMyiotK2uQdfXa2wn8/enb8IUXAvLyKnJ/Kx9AtW1sLKKj523e/GO/HjUIwVq2bHpkpC9SuGNn0vXrVQRBeHk5L1iANpZCoYz7Ouvbb3/rJm3TFX8kx9XFftOmRTNmjEWOj14z5/LlCqVS53Ihl8vZ/unTL70UV1vXqrfCSqUqKblwxaticqE42IscLC6XM3PmOKTOZ8/d0vWczc3Sy5fRnk5Dc2Vw8FBGYKAn+ZwU9cy0hIRc9a+2JNJXINDaDjMru7SwsMbLc8SiRT7U3yIuLvOYdhseOXq/DTduXBgQMA45fs2aOZfy7uhqw8TE/N4H4zATE+7MgHFI5zMnzHvkSJu6OqatYhGPGiwfHxfk4osgiO+P5/3883X116tWBSO9jEKhXL/+RHZOKfXZqqola6L/s2VLxFOLp5DLvbycZ88an55RzFATGxuLmJhlK1bG9/T06a124ukbr/6PmPwHnhXsdeBAmubbKVNcbGy0ruRzc8tbWnSuEVZUNG7d9rPe1y0ursvKKpk9e7ymxNnZJijQMzunlMMhnnlGa8ilUhFfPThdrVxJ04YbNpzIyqZvw+i131Hb0NNzREjIhLS027QV27P3bHt7t/pre3vBiR/eJA9duFxOYKDHTz9d0/sLPnwI+0OpaAfs169X7dt3Xv21ublpUCA6Lo49kEabKjWlUvXZZ4nFxXVIedgc/VtXjh/v/P7mxWxqXlUtuXlTq+Pz8HD620hbzbfiILTaZx6tH9Q4eCgdOW1ERfkTBDEzwGPMGAdyeeqFopKSej7fJIhSmYOH0mlTpaZuQ/XFONmcMFbbf0oknb9RBhWjSVM8bAw8WBwOQR2w19a2bth4QjMTERAwjs/XOik2NLQfP57H/MxyueJfsWlIoTjIU+/MjXoibekSPzb1TzyDDuHFpGtDsfZ1olTak637D/lgns/C39+N+m/8eGfkyPLyxvR0rdNGYKCni4v9smXTyYVKperw4funqxkzxpmba93c0djU8d13l5h/O7lc8eW/LiCFQUGe1OEXLRWlx6SWMBt4V8jjcZEBu0zW++76H1pbZZqSse4i5FEX0m739ir0PvnlyxUSSSf5isnKytzJyYpNN79p06KKyqaOjm7mw86evbnunfnk3AcHe504eUXdPY0bp7WX7rlzN5l7WG/vkV8deplaXlBQvWLlEaTwq8OZoaHemt6NwyHefissMNCDfExKSmFlZfP9NhyLtmHaBVZtePVqZUuL1MFBqCkRCvlOTtb19XraUCjkU4doDQ+uw9gbzCWdB1fjDeQSkSO6uSi1j6OlVKpKSuqR4bOjiFWwTEy4O754Vu+IRyrtycounUeaKvT3d7O0NJPJeqnTWoPVD6pVVDSlphaFh0/SlFAnnw/HZaq/dqRs0Fpc0o82RPLq6GhFG6x178xXD945nPvtgEyCqN/qbF5UYzDvbvDzG4MMP62s0NVAqZRm5YRWhxQ95VjRrYsRBNFO2XRZJBK+v/lJvS+BTGiZmfFm/H0stR+srW2lzo8/okOH0hk2uP/19I2amnvqrx+pDSmnbWsdm2EvXjzl6aV+Ty/1W7rEz9UFHU4VFNaoT5/sDfJtM+veCSd3IvckncgB7Kf4HChvGuqzqe3afVa94kE2erSd3pe4dOlOc7PWhV5wsBefb+Lv70YuTEzMH/TbzKuqJWfP3qT9kVyuOHokR/OthNqGrKeaqSceyT36NmSgUChjYpL7+6hBDpaZmclHH0ZqrhORPxtBEMjSm+7n4Xl7/w0p1NXNt7V1rd9woptx53paCoUyOaWQXCIWewYEaA2WVSpWyzgSSWda+m3qv6vX/tT1kMNxmbQnrZ9+ukaejWumzHGwb0NkEHx/4N/PoZJcrti67WfqBaZejzrGSkouXDD/CfIsy4QJI1evnh374LLuFmV5PDRkwt6956idF2J++BPIOnFjY7tEonMaqbS0fvv2xE8+Wdrf+icm5pNvdrC3F6xcGUw+ID+/StMrMSgvb9i48WS/Xrqm5t6ZMwUREVPJhT09fQkJF8klRbfQNgwJGW9ra0m+SKI1b94ktA2bOpqbOzjsF1YfLDboOrMye6Qz1nffXdq69dR/KJe+r7wcpO5NbtyoamvTypBQyI/Wt/ZnZ2f55puhSGFGRglzf5ScUvjtt7/17xcgiDt3GpHriYnaZ8ozZwZz2I7IySlDSvLzqxqbOsglN/Kr7t3TypBAoL8NbW0t334rDCnMzNTZhpmZJampRVWUG5AWLpxMnt5jb+DB6utT7tl7jiCIgwfTkZEdl8vZti1CKOQrFMrz59FlkCVLfFetmqXraW1sLHbves7JyRopT2Hxvtn/ZWpubr+Xw6gTWhq9vX2pdLfT4ENdclEqVedT0TaMiJi6+rXZuk49utqQ4dzz0ce/vLf5x/c2nUQqYGZmsnatnhDTGoQxVm9v35atp5Dbs/420nbjhoUEQcTHZ1NHP2+8HrKXcm8Wl8sJnzfp22OrkRvi1O9sNtdlSqXqn1v+S3vfH4Pk5ELkli+NjMwSvfNhapMnu5w88ZaufwsWPNGvKiGOHMmh3k+7evXsvXv+gaxMc7mceXMnHjv22pQpaBvm5pbfuFHF/EKlZQ2//oreGDhnzkQ/vzH9rfPgzGMVF9clHMtFlnUXLfLJuVh27tythGO5q1+bjTwkONgrONirvLyxtKxB1tnjIBL6TnWlvbWmt1ex/8tUljVpb+9+Z93xfx9diazmMmhr67p4sSwkZAL1R+z7QQsLU3fKbLAG7Q3E7DU1dSQkXHz99RCkXCz2FIs9B7cNDxxMmzt3olCo1Xrr353/4ktxDDcBUA3aVWFcXGZZWQNSuPm9J52dbb7+Oos6mFDz8HBatHByVNS00JAJum7Y2hmTVFHRj5sY//ijeduHv/RrgiCRLkASSSfL+3CGwDfx2brWlNi0YUxMMss7ySSSzvgj2Uihl5czcpGh16AFSy5XbPvwF6RDtLIy//jjJQRBbNl6qrS0fgBPGx+frblRgr2MjOJvvslif3xOTikyQFZ3kQxzmENMqVRt2XqqpGQgbXjkaM6pn39nf/z33+dRR/FvvRmGnMaYDeY8VmlpPTXsfr5jXng+oKOje+WqI2np9Pds0JLLFZ98elp9V+4AfHU4g/Y2dlp9fcpz59CB7eAu4zw6qbTn1RXxKSn9uPhXKJQ7Y5JjKSv6zORyxf79aL9pby9YsSKY/ZOwHWPJZD3Iajntcnd8fLZM1ovchqBQKLlcTleXfNOmk5GRvqtXhzhR1hARl69U7tt3nvYNeuLkFVG61o1Zf/xBs9qgUhEff3L6VlGtZtqmq4vp/0J9fzyPfJ3f0yNnOMWWltZT7x1gUFBQQ1tefqcReZ7au0xzZg+uk/57+UrFG2+E6m3DK1cq9+1PpS7OqlQqauW7u7WW2DMyinfsSLLUHqfKe/u4XA7LkZYBPm3G3Nw04qmpoaET/PzGIBFsa+vKzi5NTimk/r8DQGZubvrU4imhYd7+lDZsb+/Kzi5LTik07ADRkB9jZGVl7jLaXiQSCq3MW1qkTY0df1a1/HWGNUZBKOS7ujj8BdvQCD4fCxgj+FAQgAUEC2ABwQJYQLAAFhAsgAUEC2ABwQJYQLAAFhAsgAUEC2ABwQJYQLAAFhAsgAUEC2ABwQJYQLAAFhAsgAUEC2ABwQJYQLAAFv8XAAD//xeIBgIYa1MwAAAAAElFTkSuQmCC', + csvName: 'gitops-primer.v0.0.1', + operatorKind: 'Export', + }, + }, + }; + mockOperatorBackedServiceNode.setModel(OperatorBackedServiceModel); + const { csvName } = mockOperatorBackedServiceNode.getData()?.data; + const resource = mockOperatorBackedServiceNode.getResource(); + const expectedContext = { + 'topology-actions': mockOperatorBackedServiceNode, + [getReferenceForResource(resource)]: resource, + 'csv-actions': { + csvName, + resource, + }, + }; + expect(contextMenuActions(mockOperatorBackedServiceNode)).toEqual(expectedContext); + }); +}); diff --git a/src/actions/contextMenuActions.tsx b/src/actions/contextMenuActions.tsx new file mode 100644 index 0000000..db6eef5 --- /dev/null +++ b/src/actions/contextMenuActions.tsx @@ -0,0 +1,61 @@ +import React, { Fragment } from 'react'; + +import { + Action, + GroupedMenuOption, + MenuOption, + MenuOptionType, +} from '@openshift-console/dynamic-plugin-sdk'; +import { ContextMenuItem, ContextSubMenuItem, Graph, Node } from '@patternfly/react-topology'; + +import ActionMenuItem from '../components/ActionMenu/components/ActionMenuContent/components/ActionMenuItem/ActionMenuItem'; +import { + getMenuOptionType, + orderExtensionBasedOnInsertBeforeAndAfter, +} from '../components/ActionMenu/components/ActionMenuContent/utils/utils'; +import { getResource } from '../utils'; +import { getReferenceForResource } from '../utils/k8s-utils'; + +export const createContextMenuItems = (actions: MenuOption[]) => { + const sortedOptions = orderExtensionBasedOnInsertBeforeAndAfter(actions); + return sortedOptions.map((option: MenuOption) => { + const optionType = getMenuOptionType(option); + switch (optionType) { + case MenuOptionType.SUB_MENU: + return ( + + {createContextMenuItems((option as GroupedMenuOption).children)} + + ); + case MenuOptionType.GROUP_MENU: + return ( + + {option.label &&

{option.label}

} + {createContextMenuItems((option as GroupedMenuOption).children)} +
+ ); + default: + return ( + + ); + } + }); +}; + +export const graphActionContext = (graph: Graph, connectorSource?: Node) => ({ + 'topology-context-actions': { element: graph, connectorSource }, +}); + +export const groupActionContext = (element: Node, connectorSource?: Node) => ({ + 'topology-context-actions': { element, connectorSource }, +}); + +export const contextMenuActions = (element: Node) => { + const resource = getResource(element); + const { csvName } = element.getData()?.data ?? {}; + return { + 'topology-actions': element, + ...(resource ? { [getReferenceForResource(resource)]: resource } : {}), + ...(csvName ? { 'csv-actions': { csvName, resource } } : {}), + }; +}; diff --git a/src/actions/edgeActions.ts b/src/actions/edgeActions.ts new file mode 100644 index 0000000..888a91a --- /dev/null +++ b/src/actions/edgeActions.ts @@ -0,0 +1,116 @@ +import { Action, K8sModel } from '@openshift-console/dynamic-plugin-sdk'; +import { getK8sModel } from '@openshift-console/dynamic-plugin-sdk/lib/utils/k8s/hooks/useK8sModel'; +import { Edge, isNode, Node } from '@patternfly/react-topology'; +import { t } from '@topology-utils/hooks/useTopologyTranslation'; +import { asAccessReview } from '@topology-utils/resources/shared'; + +import { moveConnectionModal } from '../components/modals/MoveConnectionModal'; +import { TYPE_CONNECTS_TO, TYPE_SERVICE_BINDING, TYPE_TRAFFIC_CONNECTOR } from '../const'; +import { removeConnection } from '../utils'; +import { getResource } from '../utils'; +import { + TYPE_EVENT_PUB_SUB, + TYPE_EVENT_SOURCE, + TYPE_EVENT_SOURCE_LINK, + TYPE_KAFKA_CONNECTION_LINK, + TYPE_KNATIVE_REVISION, + TYPE_KNATIVE_SERVICE, + TYPE_MANAGED_KAFKA_CONNECTION, + TYPE_REVISION_TRAFFIC, +} from '../utils/knative/knative-const'; + +/** + * @deprecated migrated to use new Action extension, use MoveConnectorAction + */ +const moveConnection = (edge: Edge, availableTargets: Node[]) => { + const resourceObj = getResource(edge.getSource()); + const resourceModel = getK8sModel(resourceObj); + + return { + labelKey: t('Move connector'), + callback: () => { + moveConnectionModal({ edge, availableTargets }); + }, + isDisabled: availableTargets.length <= 1, + accessReview: asAccessReview(resourceModel, resourceObj, 'delete'), + }; +}; + +const getAvailableTargetForEdge = (edge: Edge, nodes: Node[]) => { + const currentTargets = edge + .getSource() + .getSourceEdges() + .map((e) => e.getTarget().getId()); + + return nodes + .filter((n) => { + if (n.getId() === edge.getSource().getId()) { + return false; + } + if (n.getId() !== edge.getTarget().getId() && currentTargets.includes(n.getId())) { + return false; + } + if (n.getType() === TYPE_EVENT_SOURCE) { + return false; + } + switch (edge.getType()) { + case TYPE_CONNECTS_TO: + return n.getType() !== TYPE_KNATIVE_REVISION && n.getType() !== TYPE_KNATIVE_SERVICE; + case TYPE_SERVICE_BINDING: + return false; + case TYPE_EVENT_SOURCE_LINK: + return n.getType() === TYPE_KNATIVE_SERVICE || n.getType() === TYPE_EVENT_PUB_SUB; + case TYPE_REVISION_TRAFFIC: + return false; + case TYPE_TRAFFIC_CONNECTOR: + return false; + case TYPE_KAFKA_CONNECTION_LINK: + return n.getType() === TYPE_MANAGED_KAFKA_CONNECTION; + default: + return true; + } + }) + .sort((n1, n2) => n1.getLabel().localeCompare(n2.getLabel())); +}; + +export const MoveConnectorAction = (kindObj: K8sModel, element: Edge): Action => { + const resourceObj = getResource(element.getSource()); + + const nodes = element + .getController() + .getElements() + .filter((e) => isNode(e) && !e.isGroup()) as Node[]; + const availableTargets = getAvailableTargetForEdge(element, nodes); + + return { + id: 'move-visual-connector', + label: t('Move connector'), + cta: () => { + moveConnectionModal({ edge: element, availableTargets }); + }, + disabled: availableTargets.length <= 1, + accessReview: asAccessReview(kindObj, resourceObj, 'delete'), + }; +}; + +export const DeleteConnectorAction = (kindObj: K8sModel, element: Edge): Action => { + const resourceObj = getResource(element.getSource()); + return { + id: 'delete-connector', + label: t('Delete connector'), + cta: () => { + removeConnection(element); + }, + accessReview: asAccessReview(kindObj, resourceObj, 'delete'), + }; +}; + +/** + * @deprecated remove this after migrating the Traffic connector side-panel to dynamic extensions + */ +export const edgeActions = (edge: Edge, nodes: Node[]): KebabOption[] => { + const actions: KebabOption[] = []; + const availableTargets = getAvailableTargetForEdge(edge, nodes); + actions.push(moveConnection(edge, availableTargets)); + return actions; +}; diff --git a/src/actions/index.ts b/src/actions/index.ts new file mode 100644 index 0000000..89e67da --- /dev/null +++ b/src/actions/index.ts @@ -0,0 +1,5 @@ +export * from './edgeActions'; +export * from './nodeActions'; +export * from './modify-application'; +export * from './contextMenuActions'; +export * from './provider'; diff --git a/src/actions/modify-application.ts b/src/actions/modify-application.ts new file mode 100644 index 0000000..51856cc --- /dev/null +++ b/src/actions/modify-application.ts @@ -0,0 +1,30 @@ +import { Action, K8sKind } from '@openshift-console/dynamic-plugin-sdk'; +import { t } from '@topology-utils/hooks/useTopologyTranslation'; +import { K8sResourceKind } from '@topology-utils/types/k8s-types'; + +import { editApplicationModal } from '../components/modals'; + +export const getModifyApplicationAction = ( + kind: K8sKind, + obj: K8sResourceKind, + insertBefore?: string | string[], +): Action => { + return { + id: 'modify-application', + label: t('Edit application grouping'), + insertBefore: insertBefore ?? 'edit-pod-count', + cta: () => + editApplicationModal({ + resourceKind: kind, + resource: obj, + blocking: true, + initialApplication: '', + }), + accessReview: { + verb: 'patch', + group: kind.apiGroup, + resource: kind.plural, + namespace: obj?.metadata?.namespace, + }, + }; +}; diff --git a/src/actions/nodeActions.ts b/src/actions/nodeActions.ts new file mode 100644 index 0000000..1b5227f --- /dev/null +++ b/src/actions/nodeActions.ts @@ -0,0 +1,13 @@ +import { getGroupVersionKindForResource } from '@openshift-console/dynamic-plugin-sdk'; +import { getK8sModel } from '@openshift-console/dynamic-plugin-sdk/lib/utils/k8s/hooks/useK8sModel'; +import { K8sResourceKind } from '@topology-utils/types/k8s-types'; + +export const nodeActions = (contextMenuResource: K8sResourceKind): KebabOption[] => { + if (!contextMenuResource) { + return null; + } + const resourceKind = getK8sModel(getGroupVersionKindForResource(contextMenuResource)); + const menuActions = [...Kebab.getExtensionsActionsForKind(resourceKind), ...Kebab.factory.common]; + + return menuActions?.map((a) => a(resourceKind, contextMenuResource)); +}; diff --git a/src/actions/provider.ts b/src/actions/provider.ts new file mode 100644 index 0000000..dc87442 --- /dev/null +++ b/src/actions/provider.ts @@ -0,0 +1,45 @@ +import { useMemo } from 'react'; + +import { getGroupVersionKindForResource } from '@openshift-console/dynamic-plugin-sdk'; +import { + getK8sModel, + useK8sModel, +} from '@openshift-console/dynamic-plugin-sdk/lib/utils/k8s/hooks/useK8sModel'; +import { Edge, GraphElement } from '@patternfly/react-topology'; + +import { TYPE_CONNECTS_TO, TYPE_WORKLOAD } from '../const'; +import { getResource } from '../utils'; + +import { DeleteConnectorAction, MoveConnectorAction } from './edgeActions'; +import { getModifyApplicationAction } from './modify-application'; + +export const useTopologyWorkloadActionProvider = (element: GraphElement) => { + const resource = getResource(element); + const actions = useMemo(() => { + if (element.getType() !== TYPE_WORKLOAD) return undefined; + if (!resource) { + return []; + } + const k8sKind = getK8sModel(resource); + return [getModifyApplicationAction(k8sKind, resource)]; + }, [element, resource]); + + return useMemo(() => { + if (!actions) return [[], true, undefined]; + return [actions, true, undefined]; + }, [actions]); +}; + +export const useTopologyVisualConnectorActionProvider = (element: Edge) => { + const resource = getResource(element.getSource?.()); + const [kindObj, inFlight] = useK8sModel(getGroupVersionKindForResource(resource)); + const actions = useMemo(() => { + if (!kindObj || element.getType() !== TYPE_CONNECTS_TO) return undefined; + return [MoveConnectorAction(kindObj, element), DeleteConnectorAction(kindObj, element)]; + }, [element, kindObj]); + + return useMemo(() => { + if (!actions) return [[], true, undefined]; + return [actions, !inFlight, undefined]; + }, [actions, inFlight]); +}; diff --git a/src/behavior/index.ts b/src/behavior/index.ts new file mode 100644 index 0000000..3d1b566 --- /dev/null +++ b/src/behavior/index.ts @@ -0,0 +1,2 @@ +export { default as useHover } from './useHover'; +export * from './withCreateConnector'; diff --git a/src/behavior/useHover.ts b/src/behavior/useHover.ts new file mode 100644 index 0000000..987ecec --- /dev/null +++ b/src/behavior/useHover.ts @@ -0,0 +1,95 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; + +import { useCallbackRef } from '@patternfly/react-topology'; + +// +// Local version of the @patternfly/react-topology useHover +// Updated to provide the capability to reset the hover state based on a dependency change +// + +const EMPTY: any[] = []; + +const useHover = ( + delayIn = 200, + delayOut = 200, + dependencies: any[] = EMPTY, +): [boolean, (node: T) => (() => void) | undefined] => { + const [hover, setHover] = useState(false); + const mountRef = useRef(true); + + // need to ensure we do not start the unset timer on unmount + useEffect( + () => () => { + mountRef.current = false; + }, + [], + ); + + useEffect(() => { + setHover(false); + // dynamic dependencies + // eslint-disable-next-line react-hooks/exhaustive-deps + }, dependencies); + + // The unset handle needs to be referred by listeners in different closures. + const unsetHandle = useRef(); + + const callbackRef = useCallbackRef( + useCallback( + (node: T) => { + if (node) { + // store locally instead of a ref because it only needs to be referred by inner closures + let delayHandle: any; + + const delayedStateChange = (newState: boolean, delay: number) => { + clearTimeout(unsetHandle.current); + clearTimeout(delayHandle); + + if (delay != null) { + delayHandle = (window as any).setTimeout(() => { + clearTimeout(unsetHandle.current); + setHover(newState); + }, delay); + } else { + setHover(newState); + } + }; + + const onMouseEnter = () => { + delayedStateChange(true, delayIn); + }; + + const onMouseLeave = () => { + delayedStateChange(false, delayOut); + }; + + node.addEventListener('mouseenter', onMouseEnter); + node.addEventListener('mouseleave', onMouseLeave); + + return () => { + node.removeEventListener('mouseenter', onMouseEnter); + node.removeEventListener('mouseleave', onMouseLeave); + clearTimeout(delayHandle); + if (mountRef.current) { + // Queue the unset in case reattaching to a new node in the same location. + // This can happen with layers. Rendering a node to a new layer will unmount the old node + // and remount a new node at the same location. This will prevent flickering and getting + // stuck in a hover state. + unsetHandle.current = (window as any).setTimeout(() => { + if (mountRef.current) { + setHover(false); + } + }, Math.max(delayIn, delayOut)); + } + }; + } + return undefined; + }, + [delayIn, delayOut], + ), + ); + + return [hover, callbackRef]; +}; + +export default useHover; diff --git a/src/behavior/withCreateConnector.tsx b/src/behavior/withCreateConnector.tsx new file mode 100644 index 0000000..54b7aab --- /dev/null +++ b/src/behavior/withCreateConnector.tsx @@ -0,0 +1,336 @@ +import React, { + ComponentProps, + ComponentType, + FunctionComponent, + isValidElement, + ReactElement, + useCallback, + useMemo, + useRef, + useState, +} from 'react'; +import { observer } from 'mobx-react'; + +import { css } from '@patternfly/react-styles'; +import styles from '@patternfly/react-styles/css/components/Topology/topology-components'; +import { + AnchorEnd, + ContextMenu, + ContextMenuItem, + DefaultCreateConnector, + DragEvent, + DragObjectWithType, + DragOperationWithType, + DragSourceMonitor, + DragSourceSpec, + DragSpecOperationType, + Graph, + GraphElement, + hullPath, + isGraph, + isNode, + LabelPosition, + Layer, + Node, + Point, + TOP_LAYER, + useCombineRefs, + useDndDrag, + useHover, +} from '@patternfly/react-topology'; + +// +// Local version of the @patternfly/react-topology withCreateConnector +// Updated to notify the wrapped component when the create connector is being dragged +// + +export const CREATE_CONNECTOR_OPERATION = '#createconnector#'; +export const CREATE_CONNECTOR_DROP_TYPE = '#createConnector#'; + +export interface ConnectorChoice { + label: string; +} + +export interface CreateConnectorOptions { + handleAngle?: number; + handleAngleTop?: number; + handleLength?: number; + dragItem?: DragObjectWithType; + dragOperation?: DragOperationWithType; + hideConnectorMenu?: boolean; +} + +interface ConnectorComponentProps { + startPoint: Point; + endPoint: Point; + hints: string[]; + dragging: boolean; + hover?: boolean; +} + +type CreateConnectorRenderer = ComponentType; + +type OnCreateResult = ConnectorChoice[] | void | undefined | null | ReactElement[]; + +type CreateConnectorWidgetProps = { + element: Node; + onKeepAlive: (isAlive: boolean) => void; + onCreate: ( + element: Node, + target: Node | Graph, + event: DragEvent, + dropHints?: string[] | undefined, + choice?: ConnectorChoice, + ) => Promise | OnCreateResult; + ConnectorComponent: CreateConnectorRenderer; + contextMenuClass?: string; +} & CreateConnectorOptions; + +interface CollectProps { + event?: DragEvent; + dragging: boolean; + hints?: string[] | undefined; +} + +interface PromptData { + element: Node; + target: Node | Graph; + event: DragEvent; + choices: ConnectorChoice[] | ReactElement[]; +} + +const isReactElementArray = ( + choices: ConnectorChoice[] | ReactElement[], +): choices is ReactElement[] => isValidElement(choices[0]); + +const DEFAULT_HANDLE_ANGLE = Math.PI / 180; +const DEFAULT_HANDLE_ANGLE_TOP = 1.5 * Math.PI; +const DEFAULT_HANDLE_LENGTH = 32; + +const CreateConnectorWidget: FunctionComponent = observer((props) => { + const { + element, + onKeepAlive, + onCreate, + ConnectorComponent, + handleAngle = DEFAULT_HANDLE_ANGLE, + handleAngleTop = DEFAULT_HANDLE_ANGLE_TOP, + handleLength = DEFAULT_HANDLE_LENGTH, + contextMenuClass, + dragItem, + dragOperation, + hideConnectorMenu, + } = props; + const [prompt, setPrompt] = useState(null); + const [active, setActive] = useState(false); + const hintsRef = useRef(); + + const spec = useMemo(() => { + const dragSourceSpec: DragSourceSpec< + DragObjectWithType, + DragSpecOperationType, + GraphElement, + CollectProps, + CreateConnectorWidgetProps + > = { + item: dragItem || { type: CREATE_CONNECTOR_DROP_TYPE }, + operation: dragOperation || { type: CREATE_CONNECTOR_OPERATION }, + begin: (monitor: DragSourceMonitor, dragProps: any) => { + setActive(true); + return dragProps.element; + }, + drag: (event: DragEvent, monitor: DragSourceMonitor, p: CreateConnectorWidgetProps) => { + p.element.raise(); + }, + end: async ( + dropResult: GraphElement, + monitor: DragSourceMonitor, + dragProps: CreateConnectorWidgetProps, + ) => { + const event = monitor.getDragEvent(); + if ((isNode(dropResult) || isGraph(dropResult)) && event) { + const choices = await dragProps.onCreate( + dragProps.element, + dropResult, + event, + monitor.getDropHints(), + ); + if (choices && choices.length && !hideConnectorMenu) { + setPrompt({ element: dragProps.element, target: dropResult, event, choices }); + return; + } + } + setActive(false); + dragProps.onKeepAlive(false); + }, + collect: (monitor) => ({ + dragging: !!monitor.getItem(), + event: monitor.isDragging() ? monitor.getDragEvent() : undefined, + hints: monitor.getDropHints(), + }), + }; + return dragSourceSpec; + }, [setActive, dragItem, dragOperation, hideConnectorMenu]); + const [{ dragging, event, hints }, dragRef] = useDndDrag(spec, props); + const [hover, hoverRef] = useHover(); + const refs = useCombineRefs(dragRef, hoverRef); + + if (!active && dragging && !event) { + // another connector is dragging right now + return null; + } + + if (dragging) { + // store the latest hints + hintsRef.current = hints; + } + + const dragEvent = prompt ? prompt.event : event; + + let startPoint: Point; + let endPoint: Point; + + if (dragEvent) { + endPoint = new Point(dragEvent.x, dragEvent.y); + startPoint = element.getAnchor(AnchorEnd.source).getLocation(endPoint); + } else { + const bounds = element.getBounds(); + const isRightLabel = element.getLabelPosition() === LabelPosition.right; + const referencePoint = isRightLabel + ? new Point(bounds.x + bounds.width / 2, bounds.y) + : new Point( + bounds.right(), + Math.tan(handleAngle) * (bounds.width / 2) + bounds.y + bounds.height / 2, + ); + startPoint = element.getAnchor(AnchorEnd.source).getLocation(referencePoint); + endPoint = new Point( + Math.cos(isRightLabel ? handleAngleTop : handleAngle) * handleLength + startPoint.x, + Math.sin(isRightLabel ? handleAngleTop : handleAngle) * handleLength + startPoint.y, + ); + } + + // bring into the coordinate space of the element + element.translateFromParent(startPoint); + element.translateFromParent(endPoint); + + const connector = ( + onKeepAlive(true) : undefined} + onMouseLeave={!active ? () => onKeepAlive(false) : undefined} + > + + + + ); + + return ( + <> + {active ? {connector} : connector} + {prompt && ( + { + setActive(false); + onKeepAlive(false); + }} + > + {isReactElementArray(prompt.choices) + ? prompt.choices + : prompt.choices.map((c: ConnectorChoice) => ( + { + onCreate(prompt.element, prompt.target, prompt.event, hintsRef.current, c); + }} + > + {c.label} + + ))} + + )} + + ); +}); + +interface ElementProps { + element: Node; +} + +export interface WithCreateConnectorProps { + onShowCreateConnector: () => void; + onHideCreateConnector: () => void; + createConnectorDrag: boolean; +} + +export const withCreateConnector = +

( + onCreate: ComponentProps['onCreate'], + ConnectorComponent: CreateConnectorRenderer = DefaultCreateConnector, + contextMenuClass?: string, + options?: CreateConnectorOptions, + ) => + (WrappedComponent: ComponentType>) => { + const Component: FunctionComponent> = ({ + children, + ...props + }) => { + const [show, setShow] = useState(false); + const [alive, setKeepAlive] = useState(false); + const onShowCreateConnector = useCallback(() => setShow(true), []); + const onHideCreateConnector = useCallback(() => setShow(false), []); + const onKeepAlive = useCallback( + (isAlive: boolean) => { + setKeepAlive((prev) => { + if (prev && !isAlive) { + onHideCreateConnector(); + } + return isAlive; + }); + }, + [onHideCreateConnector], + ); + return ( + + {children} + {(show || alive) && ( + + )} + + ); + }; + Component.displayName = `withCreateConnector(${ + WrappedComponent.displayName || WrappedComponent.name + })`; + return observer(Component); + }; diff --git a/src/components/ActionMenu/ActionMenu.tsx b/src/components/ActionMenu/ActionMenu.tsx new file mode 100644 index 0000000..ced72ab --- /dev/null +++ b/src/components/ActionMenu/ActionMenu.tsx @@ -0,0 +1,117 @@ +import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; + +import { + Action, + checkAccess, + MenuOption, + useSafetyFirst, +} from '@openshift-console/dynamic-plugin-sdk'; +import { ActionMenuVariant } from '@openshift-console/dynamic-plugin-sdk/lib/api/internal-types'; +import { Menu, MenuContent, MenuList, Popper } from '@patternfly/react-core'; + +import ActionMenuContent from './components/ActionMenuContent/ActionMenuContent'; +import ActionMenuToggle from './components/ActionMenuToggle'; + +type ActionMenuProps = { + actions: Action[]; + options?: MenuOption[]; + isDisabled?: boolean; + variant?: ActionMenuVariant; + label?: string; +}; + +const ActionMenu: FC = ({ + actions, + options, + isDisabled, + variant = ActionMenuVariant.KEBAB, + label, +}) => { + const isKebabVariant = variant === ActionMenuVariant.KEBAB; + const [isVisible, setVisible] = useSafetyFirst(isKebabVariant); + const [isOpen, setIsOpen] = useState(false); + const menuRef = useRef(null); + const toggleRef = useRef(null); + const containerRef = useRef(null); + const menuOptions = options?.length > 0 ? options : actions; + + const hideMenu = () => { + setIsOpen(false); + }; + + const handleHover = useCallback(() => { + // Check access when hovering over a kebab to minimize flicker when opened. + // This depends on `checkAccess` being memoized. + actions?.forEach((action: Action) => { + if (action.accessReview) { + checkAccess(action.accessReview).catch((e) => + // eslint-disable-next-line no-console + console.warn('Could not check access for action menu', e), + ); + } + }); + }, [actions]); + + // Check if any actions are visible when actions have access reviews. + useEffect(() => { + if (!actions.length) { + setVisible(false); + return; + } + // Do nothing if variant is kebab. The action menu should be visible and acces review happens on hover. + if (isKebabVariant) return; + + const promises = actions.reduce((acc, action) => { + if (action.accessReview) { + acc.push(checkAccess(action.accessReview)); + } + return acc; + }, []); + + // Only need to resolve if all actions require access review + if (promises.length !== actions.length) { + setVisible(true); + return; + } + Promise.all(promises) + .then((results) => setVisible(results?.some((result) => Boolean(result?.status?.allowed)))) + .catch(() => setVisible(true)); + }, [actions, isKebabVariant, setVisible]); + + const menu = ( +

+ + + + + + + ); + + return ( + isVisible && ( +
+ + +
+ ) + ); +}; + +export default ActionMenu; diff --git a/src/components/ActionMenu/components/ActionMenuContent/ActionMenuContent.tsx b/src/components/ActionMenu/components/ActionMenuContent/ActionMenuContent.tsx new file mode 100644 index 0000000..da9dbc0 --- /dev/null +++ b/src/components/ActionMenu/components/ActionMenuContent/ActionMenuContent.tsx @@ -0,0 +1,102 @@ +import React, { FC } from 'react'; + +import { + Action, + GroupedMenuOption, + MenuOption, + MenuOptionType, +} from '@openshift-console/dynamic-plugin-sdk'; +import { Divider, Menu, MenuContent, MenuGroup, MenuItem, MenuList } from '@patternfly/react-core'; + +import ActionMenuItem from './components/ActionMenuItem/ActionMenuItem'; +import { getMenuOptionType, orderExtensionBasedOnInsertBeforeAndAfter } from './utils/utils'; + +type GroupMenuContentProps = { + option: GroupedMenuOption; + onClick: () => void; +}; + +// Need to keep this in the same file to avoid circular dependency. +const GroupMenuContent: FC = ({ option, onClick }) => ( + <> + + + + + + + +); + +// Need to keep this in the same file to avoid circular dependency. +const SubMenuContent: FC = ({ option, onClick }) => ( + + + + + + + + } + translate="no" + > + {option.label} + +); + +type ActionMenuContentProps = { + options: MenuOption[]; + onClick: () => void; + focusItem?: MenuOption; +}; + +const ActionMenuContent: FC = ({ options, onClick, focusItem }) => { + const sortedOptions = orderExtensionBasedOnInsertBeforeAndAfter(options); + return ( + <> + {sortedOptions.map((option) => { + const optionType = getMenuOptionType(option); + switch (optionType) { + case MenuOptionType.SUB_MENU: + return ( + + ); + case MenuOptionType.GROUP_MENU: + return ( + + ); + default: + return ( + + ); + } + })} + + ); +}; + +export default ActionMenuContent; diff --git a/src/components/ActionMenu/components/ActionMenuContent/components/ActionMenuItem/ActionMenuItem.tsx b/src/components/ActionMenu/components/ActionMenuContent/components/ActionMenuItem/ActionMenuItem.tsx new file mode 100644 index 0000000..6ef6f1e --- /dev/null +++ b/src/components/ActionMenu/components/ActionMenuContent/components/ActionMenuItem/ActionMenuItem.tsx @@ -0,0 +1,46 @@ +import React, { ComponentType, FC } from 'react'; + +import { Action } from '@openshift-console/dynamic-plugin-sdk'; +import { DropdownItemProps, MenuItemProps, Tooltip } from '@patternfly/react-core'; + +import AccessReviewActionItem from './components/AccessReviewActionItem'; +import ActionItem from './components/ActionItem'; + +export type ActionMenuItemProps = { + action: Action; + component?: ComponentType; + autoFocus?: boolean; + onClick?: () => void; + onEscape?: () => void; +}; + +const ActionMenuItem: FC = (props) => { + const { action } = props; + let item; + + if (action.accessReview) { + item = ; + } else { + item = ; + } + + if (action?.tooltip) { + return ( + + {item} + + ); + } + + if (action?.disabled && action?.disabledTooltip) { + return ( + +
{item}
+
+ ); + } + + return item; +}; + +export default ActionMenuItem; diff --git a/src/components/ActionMenu/components/ActionMenuContent/components/ActionMenuItem/components/AccessReviewActionItem.tsx b/src/components/ActionMenu/components/ActionMenuContent/components/ActionMenuItem/components/AccessReviewActionItem.tsx new file mode 100644 index 0000000..7d19a2b --- /dev/null +++ b/src/components/ActionMenu/components/ActionMenuContent/components/ActionMenuItem/components/AccessReviewActionItem.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { connect } from 'react-redux'; + +import { useAccessReview } from '@openshift-console/dynamic-plugin-sdk'; +import { impersonateStateToProps } from '@openshift-console/dynamic-plugin-sdk/lib/app/core/reducers'; +import { ImpersonateKind } from '@openshift-console/dynamic-plugin-sdk/lib/app/redux-types'; + +import { ActionMenuItemProps } from '../ActionMenuItem'; + +import ActionItem from './ActionItem'; + +const AccessReviewActionItem = connect(impersonateStateToProps)( + (props: ActionMenuItemProps & { impersonate: ImpersonateKind }) => { + const { action, impersonate } = props; + const [isAllowed] = useAccessReview(action.accessReview, impersonate); + return ; + }, +); + +export default AccessReviewActionItem; diff --git a/src/components/ActionMenu/components/ActionMenuContent/components/ActionMenuItem/components/ActionItem.tsx b/src/components/ActionMenu/components/ActionMenuContent/components/ActionMenuItem/components/ActionItem.tsx new file mode 100644 index 0000000..681bd53 --- /dev/null +++ b/src/components/ActionMenu/components/ActionMenuContent/components/ActionMenuItem/components/ActionItem.tsx @@ -0,0 +1,74 @@ +import React, { FC, useCallback } from 'react'; +import { useHistory } from 'react-router-dom'; +import classNames from 'classnames'; + +import { KEY_CODES, MenuItem } from '@patternfly/react-core'; + +import { ActionMenuItemProps } from '../ActionMenuItem'; + +const ActionItem: FC = ({ + action, + onClick, + onEscape, + autoFocus, + isAllowed, + component, +}) => { + const history = useHistory(); + const { label, icon, disabled, cta } = action; + const { href, external } = cta as { href: string; external?: boolean }; + const isDisabled = !isAllowed || disabled; + const classes = classNames({ 'pf-m-disabled': isDisabled }); + + const handleClick = useCallback( + (event) => { + event.preventDefault(); + if (typeof cta === 'function') { + cta(); + } else if (cta instanceof Object) { + if (!cta?.external) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + history.push(cta?.href); + } + } + onClick && onClick(); + event.stopPropagation(); + }, + [cta, onClick], + ); + + const handleKeyDown = (event) => { + if (event.keyCode === KEY_CODES.ESCAPE_KEY) { + onEscape && onEscape(); + } + + if (event.keyCode === KEY_CODES.ENTER) { + handleClick(event); + } + }; + const Component = component ?? MenuItem; + + const props = { + icon, + autoFocus, + isDisabled, + className: classes, + onClick: handleClick, + 'data-test-action': label, + translate: 'no', + }; + + const extraProps = { + onKeyDown: handleKeyDown, + ...(external ? { to: href, isExternalLink: external } : {}), + }; + + return ( + + {label} + + ); +}; + +export default ActionItem; diff --git a/src/components/ActionMenu/components/ActionMenuContent/utils/utils.ts b/src/components/ActionMenu/components/ActionMenuContent/utils/utils.ts new file mode 100644 index 0000000..6bd9c8e --- /dev/null +++ b/src/components/ActionMenu/components/ActionMenuContent/utils/utils.ts @@ -0,0 +1,105 @@ +import { + GroupedMenuOption, + MenuOption, + MenuOptionType, +} from '@openshift-console/dynamic-plugin-sdk'; + +interface ItemsToSort { + id: string; + insertBefore?: string | string[]; + insertAfter?: string | string[]; +} + +const itemDependsOnItem = (s1: T, s2: T): boolean => { + if (!s1.insertBefore && !s1.insertAfter) { + return false; + } + const before = Array.from(s1.insertBefore) || []; + const after = Array.from(s1.insertAfter) || []; + return before.includes(s2.id) || after.includes(s2.id); +}; + +const isPositioned = (item: T, allItems: T[]): boolean => + !!allItems.find((i) => itemDependsOnItem(item, i)); + +const findIndexForItem = (item: T, currentItems: T[]): number => { + const { insertBefore, insertAfter } = item; + let index = -1; + const before = Array.from(insertBefore) || []; + const after = Array.from(insertAfter) || []; + let count = 0; + while (count < before.length && index < 0) { + // eslint-disable-next-line no-loop-func + index = currentItems.findIndex((i) => i.id === before[count]); + count++; + } + count = 0; + while (count < after.length && index < 0) { + // eslint-disable-next-line no-loop-func + index = currentItems.findIndex((i) => i.id === after[count]); + if (index >= 0) { + index += 1; + } + count++; + } + return index; +}; + +const insertItem = (item: T, currentItems: T[]): void => { + const index = findIndexForItem(item, currentItems); + if (index >= 0) { + currentItems.splice(index, 0, item); + } else { + currentItems.push(item); + } +}; + +const insertPositionedItems = ( + insertItems: T[], + currentItems: T[], +): void => { + if (insertItems.length === 0) { + return; + } + + const sortedItems = insertItems.filter((item) => !isPositioned(item, insertItems)); + const positionedItems = insertItems.filter((item) => isPositioned(item, insertItems)); + + if (sortedItems.length === 0) { + // Circular dependencies + positionedItems.forEach((i) => insertItem(i, currentItems)); + return; + } + + sortedItems.forEach((i) => insertItem(i, currentItems)); + insertPositionedItems(positionedItems, currentItems); +}; + +export const orderExtensionBasedOnInsertBeforeAndAfter = ( + items: T[], +): T[] => { + if (!items || !items.length) { + return []; + } + const sortedItems = items.filter((item) => !isPositioned(item, items)); + const positionedItems = items.filter((item) => isPositioned(item, items)); + insertPositionedItems(positionedItems, sortedItems); + return sortedItems; +}; + +export const getMenuOptionType = (option: MenuOption) => { + // a grouped menu has children + const isGroupMenu = Array.isArray((option as GroupedMenuOption).children); + // a submenu menu has children and submenu property true + const isSubMenu = isGroupMenu && (option as GroupedMenuOption).submenu; + + if (isSubMenu) { + return MenuOptionType.SUB_MENU; + } + + if (isGroupMenu) { + return MenuOptionType.GROUP_MENU; + } + + return MenuOptionType.ATOMIC_MENU; +}; diff --git a/src/components/ActionMenu/components/ActionMenuToggle.tsx b/src/components/ActionMenu/components/ActionMenuToggle.tsx new file mode 100644 index 0000000..9532a10 --- /dev/null +++ b/src/components/ActionMenu/components/ActionMenuToggle.tsx @@ -0,0 +1,98 @@ +import React, { FC, RefObject, SetStateAction, useEffect } from 'react'; + +import { ActionMenuVariant } from '@openshift-console/dynamic-plugin-sdk/lib/api/internal-types'; +import { MenuToggle } from '@patternfly/react-core'; +import { EllipsisVIcon } from '@patternfly/react-icons'; +import { useTopologyTranslation } from '@topology-utils/hooks/useTopologyTranslation'; + +type ActionMenuToggleProps = { + isOpen: boolean; + isDisabled: boolean; + menuRef: RefObject; + toggleRef: RefObject; + toggleVariant?: ActionMenuVariant; + toggleTitle?: string; + onToggleClick: (state: SetStateAction) => void; + onToggleHover: () => void; +}; + +const ActionMenuToggle: FC = ({ + isOpen, + isDisabled, + menuRef, + toggleRef, + toggleVariant = ActionMenuVariant.KEBAB, + toggleTitle, + onToggleClick, + onToggleHover, +}) => { + const { t } = useTopologyTranslation(); + const isKebabVariant = toggleVariant === ActionMenuVariant.KEBAB; + const toggleLabel = toggleTitle || t('Actions'); + + const handleMenuKeys = (event) => { + if (!isOpen) { + return; + } + if (menuRef.current) { + if (event.key === 'Escape') { + onToggleClick(false); + toggleRef.current.focus(); + } + if (!menuRef.current?.contains(event.target) && event.key === 'Tab') { + onToggleClick(false); + } + } + }; + + const handleClickOutside = (event) => { + if ( + toggleRef.current !== event.target && + !toggleRef.current?.contains(event.target) && + !menuRef.current?.contains(event.target) + ) { + onToggleClick(false); + } + }; + + useEffect(() => { + if (isOpen) { + (window as any).addEventListener('keydown', handleMenuKeys); + (window as any).addEventListener('click', handleClickOutside); + } + return () => { + (window as any).removeEventListener('keydown', handleMenuKeys); + (window as any).removeEventListener('click', handleClickOutside); + }; // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isOpen]); // This needs to be run only on component mount/unmount + + const handleToggleClick = () => { + setTimeout(() => { + const firstElement = menuRef?.current?.querySelector( + 'li > button:not(:disabled)', + ); + firstElement?.focus(); + }, 0); + onToggleClick((open) => !open); + }; + + return ( + + {isKebabVariant ? : toggleLabel} + + ); +}; + +export default ActionMenuToggle; diff --git a/src/components/ActionsMenu/components/ActionsMenuDropdown.tsx b/src/components/ActionsMenu/components/ActionsMenuDropdown.tsx new file mode 100644 index 0000000..7627e93 --- /dev/null +++ b/src/components/ActionsMenu/components/ActionsMenuDropdown.tsx @@ -0,0 +1,84 @@ +import classNames from 'classnames'; +import { PropTypes } from 'prop-types'; + +Dropdown.propTypes = { + actionItems: PropTypes.arrayOf( + PropTypes.shape({ + actionKey: PropTypes.string, + actionTitle: PropTypes.string, + }), + ), + autocompleteFilter: PropTypes.func, + autocompletePlaceholder: PropTypes.string, + canFavorite: PropTypes.bool, + className: PropTypes.string, + dropDownClassName: PropTypes.string, + enableBookmarks: PropTypes.bool, + headerBefore: PropTypes.objectOf(PropTypes.string), + items: PropTypes.object.isRequired, + menuClassName: PropTypes.string, + buttonClassName: PropTypes.string, + noSelection: PropTypes.bool, + userSettingsPrefix: PropTypes.string, + storageKey: PropTypes.string, + spacerBefore: PropTypes.instanceOf(Set), + textFilter: PropTypes.string, + title: PropTypes.node, + disabled: PropTypes.bool, + id: PropTypes.string, + onChange: PropTypes.func, + selectedKey: PropTypes.string, + titlePrefix: PropTypes.string, + ariaLabel: PropTypes.string, + name: PropTypes.string, + autoSelect: PropTypes.bool, + describedBy: PropTypes.string, + required: PropTypes.bool, + dataTest: PropTypes.string, +}; + +class ActionsMenuDropdown_ extends DropdownMixin { + render() { + const { actions, title = undefined, t } = this.props; + const onClick = (event, option) => { + event.preventDefault(); + + if (option.callback) { + option.callback(); + } + + if (option.href) { + history.push(option.href); + } + + this.hide(); + }; + return ( +
+ + {this.state.active && } +
+ ); + } +} + +const ActionsMenuDropdown = withTranslation()(ActionsMenuDropdown_); diff --git a/src/components/application-panel/ApplicationGroupResource.tsx b/src/components/application-panel/ApplicationGroupResource.tsx new file mode 100644 index 0000000..5e97ecb --- /dev/null +++ b/src/components/application-panel/ApplicationGroupResource.tsx @@ -0,0 +1,48 @@ +import React, { FC } from 'react'; +import { Link } from 'react-router-dom'; + +import { useActiveNamespace } from '@openshift-console/dynamic-plugin-sdk-internal'; +import { isEmpty, size, take } from '@topology-utils/common-utils'; +import { useTopologyTranslation } from '@topology-utils/hooks/useTopologyTranslation'; +import { K8sResourceKind } from '@topology-utils/types/k8s-types'; + +import { getReferenceForResource } from '../../utils/k8s-utils'; +import SidebarSectionHeading from '../workload/JobOverview/components/SidebarSectionHeading'; + +import TopologyApplicationResourceList from './TopologyApplicationList'; + +const MAX_RESOURCES = 5; + +export type ApplicationGroupResourceProps = { + title: string; + resourcesData: K8sResourceKind[]; + group: string; +}; + +const ApplicationGroupResource: FC = ({ + title, + resourcesData, + group, +}) => { + const { t } = useTopologyTranslation(); + const [activeNamespace] = useActiveNamespace(); + return !isEmpty(resourcesData) ? ( +
+ + {size(resourcesData) > MAX_RESOURCES && ( + + {t('View all {{size}}', { size: size(resourcesData) })} + + )} + + +
+ ) : null; +}; + +export default ApplicationGroupResource; diff --git a/src/components/application-panel/TopologyApplicationList.tsx b/src/components/application-panel/TopologyApplicationList.tsx new file mode 100644 index 0000000..0e1af9e --- /dev/null +++ b/src/components/application-panel/TopologyApplicationList.tsx @@ -0,0 +1,36 @@ +import React, { FC } from 'react'; + +import { + getGroupVersionKindForResource, + ResourceLink, +} from '@openshift-console/dynamic-plugin-sdk'; +import { K8sResourceKind } from '@topology-utils/types/k8s-types'; + +type TopologyApplicationResourceListProps = { + resources: K8sResourceKind[]; +}; + +const TopologyApplicationResourceList: FC = ({ + resources, +}) => { + return ( +
    + {resources?.map((resource) => { + const { + metadata: { name, namespace, uid }, + } = resource; + return ( +
  • + +
  • + ); + })} +
+ ); +}; + +export default TopologyApplicationResourceList; diff --git a/src/components/application-panel/TopologyApplicationResources.scss b/src/components/application-panel/TopologyApplicationResources.scss new file mode 100644 index 0000000..d48eee1 --- /dev/null +++ b/src/components/application-panel/TopologyApplicationResources.scss @@ -0,0 +1,5 @@ +.odc-application-resource-tab { + & > ul > li > button { + pointer-events: none; + } + } diff --git a/src/components/application-panel/TopologyApplicationResources.tsx b/src/components/application-panel/TopologyApplicationResources.tsx new file mode 100644 index 0000000..d92c5b3 --- /dev/null +++ b/src/components/application-panel/TopologyApplicationResources.tsx @@ -0,0 +1,40 @@ +import React, { FC } from 'react'; + +import { labelKeyForNodeKind } from '../../utils/common-utils'; +import { OdcNodeModel } from '../../utils/types/topology-types'; + +import ApplicationGroupResource from './ApplicationGroupResource'; + +import './TopologyApplicationResources.scss'; + +type TopologyApplicationResourcesProps = { + resources: OdcNodeModel[]; + group: string; +}; + +const TopologyApplicationResources: FC = ({ + resources, + group, +}) => { + const resourcesData = resources.reduce((acc, { resource }) => { + if (resource?.kind) { + acc[resource.kind] = [...(acc[resource.kind] ? acc[resource.kind] : []), resource]; + } + return acc; + }, {}); + + return ( + <> + {Object.keys(resourcesData)?.map((key) => ( + + ))} + + ); +}; + +export default TopologyApplicationResources; diff --git a/src/components/application-panel/__tests__/ApplicationGroupResource.spec.tsx b/src/components/application-panel/__tests__/ApplicationGroupResource.spec.tsx new file mode 100644 index 0000000..3fbb1da --- /dev/null +++ b/src/components/application-panel/__tests__/ApplicationGroupResource.spec.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { shallow } from 'enzyme'; + +import { K8sResourceKind } from '@topology-utils/types/k8s-types'; + +import { sampleDeployments } from '../../../utils/__tests__/test-resource-data'; +import ApplicationGroupResource from '../ApplicationGroupResource'; +import TopologyApplicationResourceList from '../TopologyApplicationList'; + +describe(ApplicationGroupResource.displayName, () => { + it('should component exists', () => { + const wrapper = shallow( + , + ); + expect(wrapper.isEmptyRender()).toBe(false); + }); + + it('should not exists when resourceData is an empty array', () => { + const wrapper = shallow( + , + ); + expect(wrapper.isEmptyRender()).toBe(true); + }); + + it('should render view all link if resource is greater than MAX_RESOURCE', () => { + const resourcesData: K8sResourceKind[] = [ + { kind: 'DeploymentConfig', metadata: { name: 'a', uid: '1' } }, + { kind: 'DeploymentConfig', metadata: { name: 'b', uid: '2' } }, + { kind: 'DeploymentConfig', metadata: { name: 'c', uid: '3' } }, + { kind: 'DeploymentConfig', metadata: { name: 'd', uid: '4' } }, + { kind: 'DeploymentConfig', metadata: { name: 'e', uid: '5' } }, + { kind: 'DeploymentConfig', metadata: { name: 'f', uid: '6' } }, + { kind: 'DeploymentConfig', metadata: { name: 'g', uid: '7' } }, + ]; + const wrapper = shallow( + , + ); + expect(wrapper.find(Link).exists()).toBe(true); + }); + + it('should not render `view all` link if resource is less than MAX_RESOURCE', () => { + const resourcesData: K8sResourceKind[] = [ + { kind: 'DeploymentConfig', metadata: { name: 'a', uid: '1' } }, + ]; + const wrapper = shallow( + , + ); + expect(wrapper.find(Link).exists()).toBe(false); + }); + + it('should render TopologyApplicationResourceList if resourceData is greater than 0', () => { + const resourcesData: K8sResourceKind[] = [ + { kind: 'DeploymentConfig', metadata: { name: 'a', uid: '1' } }, + ]; + const wrapper = shallow( + , + ); + expect(wrapper.find(TopologyApplicationResourceList).exists()).toBe(true); + }); +}); diff --git a/src/components/application-panel/application-resource-link.tsx b/src/components/application-panel/application-resource-link.tsx new file mode 100644 index 0000000..8066d4d --- /dev/null +++ b/src/components/application-panel/application-resource-link.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +import { ResourceIcon } from '@openshift-console/dynamic-plugin-sdk'; +import { GraphElement } from '@patternfly/react-topology'; + +import { TYPE_APPLICATION_GROUP } from '../../const'; + +export const getApplicationPanelResourceLink = (element: GraphElement) => { + if (element.getType() !== TYPE_APPLICATION_GROUP) return undefined; + return ( + <> + + {element.getLabel()} + + ); +}; diff --git a/src/components/application-panel/index.ts b/src/components/application-panel/index.ts new file mode 100644 index 0000000..2508d42 --- /dev/null +++ b/src/components/application-panel/index.ts @@ -0,0 +1,2 @@ +export * from './application-resource-link'; +export * from './useApplicationResourceTabSection'; diff --git a/src/components/application-panel/useApplicationResourceTabSection.tsx b/src/components/application-panel/useApplicationResourceTabSection.tsx new file mode 100644 index 0000000..7bab392 --- /dev/null +++ b/src/components/application-panel/useApplicationResourceTabSection.tsx @@ -0,0 +1,25 @@ +import React from 'react'; + +import { DetailsTabSectionExtensionHook } from '@openshift-console/dynamic-plugin-sdk'; +import { GraphElement } from '@patternfly/react-topology'; + +import { TYPE_APPLICATION_GROUP } from '../../const'; + +import TopologyApplicationResources from './TopologyApplicationResources'; + +const useApplicationPanelResourceTabSection: DetailsTabSectionExtensionHook = ( + element: GraphElement, +) => { + if (element.getType() !== TYPE_APPLICATION_GROUP) { + return [undefined, true, undefined]; + } + const section = ( + + ); + return [section, true, undefined]; +}; + +export default useApplicationPanelResourceTabSection; diff --git a/src/components/common/CopyToClipboard.tsx b/src/components/common/CopyToClipboard.tsx new file mode 100644 index 0000000..c4db8d9 --- /dev/null +++ b/src/components/common/CopyToClipboard.tsx @@ -0,0 +1,55 @@ +import React, { FC, memo, ReactNode, useState } from 'react'; +import { CopyToClipboard as CTC } from 'react-copy-to-clipboard'; + +import { Button, CodeBlock, CodeBlockAction, CodeBlockCode, Tooltip } from '@patternfly/react-core'; +import { CopyIcon } from '@patternfly/react-icons'; +import { isNil } from '@topology-utils/common-utils'; +import { useTopologyTranslation } from '@topology-utils/hooks/useTopologyTranslation'; + +export type CopyToClipboardProps = { + value: string; + visibleValue?: ReactNode; +}; + +const CopyToClipboard: FC = memo((props) => { + const [copied, setCopied] = useState(false); + + const { t } = useTopologyTranslation(); + const tooltipText = copied ? t('Copied') : t('Copy to clipboard'); + const tooltipContent = [ + + {tooltipText} + , + ]; + + // Default to value if no visible value was specified. + const visibleValue = isNil(props.visibleValue) ? props.value : props.visibleValue; + + const actions = ( + + + setCopied(true)}> + + + + + ); + + return ( + + + {visibleValue} + + + ); +}); + +export default CopyToClipboard; diff --git a/src/components/common/DeploymentConfigDetailsList.tsx b/src/components/common/DeploymentConfigDetailsList.tsx new file mode 100644 index 0000000..24df0c8 --- /dev/null +++ b/src/components/common/DeploymentConfigDetailsList.tsx @@ -0,0 +1,85 @@ +import React, { FC } from 'react'; +import get from 'lodash.get'; + +import { useTopologyTranslation } from '@topology-utils/hooks/useTopologyTranslation'; + +import RuntimeClass from './DeploymentDetailsList/components/RuntimeClass'; +import DetailsItem from './DetailsItem/DetailsItem'; +import PodDisruptionBudgetField from './PodDisruptionBudgetField/PodDisruptionBudgetField'; + +type DeploymentConfigDetailsListProps = { + dc: any; +}; + +const DeploymentConfigDetailsList: FC = ({ dc }) => { + const { t } = useTopologyTranslation(); + const timeout = get(dc, 'spec.strategy.rollingParams.timeoutSeconds'); + const updatePeriod = get(dc, 'spec.strategy.rollingParams.updatePeriodSeconds'); + const interval = get(dc, 'spec.strategy.rollingParams.intervalSeconds'); + const triggers = dc.spec.triggers + ?.reduce((acc, trigger) => acc.push(trigger?.type), []) + .join(', '); + return ( +
+ + + + {dc.spec.strategy.type === 'Rolling' && ( + <> + + {t('{{count}} second', { count: timeout })} + + + {t('{{count}} second', { count: updatePeriod })} + + + {t('{{count}} second', { count: interval })} + + + {t('{{maxUnavailable}} of {{count}} pod', { + maxUnavailable: dc.spec.strategy.rollingParams.maxUnavailable ?? 1, + count: dc.spec.replicas, + })} + + + {t('{{maxSurge}} greater than {{count}} pod', { + maxSurge: dc.spec.strategy.rollingParams.maxSurge ?? 1, + count: dc.spec.replicas, + })} + + + )} + + {dc.spec.minReadySeconds + ? t('{{count}} second', { count: dc.spec.minReadySeconds }) + : t('Not configured')} + + + {triggers} + + + +
+ ); +}; + +export default DeploymentConfigDetailsList; diff --git a/src/components/common/DeploymentDetailsList/DeploymentDetailsList.tsx b/src/components/common/DeploymentDetailsList/DeploymentDetailsList.tsx new file mode 100644 index 0000000..da5ac8f --- /dev/null +++ b/src/components/common/DeploymentDetailsList/DeploymentDetailsList.tsx @@ -0,0 +1,64 @@ +import React, { FC } from 'react'; + +import { useTopologyTranslation } from '@topology-utils/hooks/useTopologyTranslation'; +import { DeploymentKind } from '@topology-utils/types/commonTypes'; + +import DetailsItem from '../DetailsItem/DetailsItem'; +import PodDisruptionBudgetField from '../PodDisruptionBudgetField/PodDisruptionBudgetField'; + +import RuntimeClass from './components/RuntimeClass'; + +type DeploymentDetailsListProps = { + deployment: DeploymentKind; +}; + +const DeploymentDetailsList: FC = ({ deployment }) => { + const { t } = useTopologyTranslation(); + return ( +
+ + {deployment.spec.strategy.type === 'RollingUpdate' && ( + <> + + {t('{{maxUnavailable}} of {{count}} pod', { + maxUnavailable: deployment.spec.strategy.rollingUpdate.maxUnavailable ?? 1, + count: deployment.spec.replicas, + })} + + + {t('{{maxSurge}} greater than {{count}} pod', { + maxSurge: deployment.spec.strategy.rollingUpdate.maxSurge ?? 1, + count: deployment.spec.replicas, + })} + + + )} + + {deployment.spec.progressDeadlineSeconds + ? t('{{count}} second', { count: deployment.spec.progressDeadlineSeconds }) + : t('Not configured')} + + + {deployment.spec.minReadySeconds + ? t('{{count}} second', { count: deployment.spec.minReadySeconds }) + : t('Not configured')} + + + +
+ ); +}; + +export default DeploymentDetailsList; diff --git a/src/components/common/DeploymentDetailsList/components/RuntimeClass.tsx b/src/components/common/DeploymentDetailsList/components/RuntimeClass.tsx new file mode 100644 index 0000000..fc495a8 --- /dev/null +++ b/src/components/common/DeploymentDetailsList/components/RuntimeClass.tsx @@ -0,0 +1,25 @@ +import React, { FC } from 'react'; + +import { K8sResourceCommon } from '@openshift-console/dynamic-plugin-sdk'; +import { useTopologyTranslation } from '@topology-utils/hooks/useTopologyTranslation'; + +import DetailsItem from '../../DetailsItem/DetailsItem'; + +export type RuntimeClassProps = { + obj: K8sResourceCommon; + path?: string; +}; + +const RuntimeClass: FC = ({ obj, path }) => { + const { t } = useTopologyTranslation(); + return ( + + ); +}; + +export default RuntimeClass; diff --git a/src/components/common/DetailsItem/DetailsItem.tsx b/src/components/common/DetailsItem/DetailsItem.tsx new file mode 100644 index 0000000..44fb7c1 --- /dev/null +++ b/src/components/common/DetailsItem/DetailsItem.tsx @@ -0,0 +1,109 @@ +import React, { FC, MouseEvent, ReactNode } from 'react'; +import classNames from 'classnames'; +import get from 'lodash.get'; + +import { getK8sModel } from '@openshift-console/dynamic-plugin-sdk/lib/utils/k8s/hooks/useK8sModel'; +import { Button, Popover, Split, SplitItem } from '@patternfly/react-core'; +import { isEmpty } from '@topology-utils/common-utils'; +import { useTopologyTranslation } from '@topology-utils/hooks/useTopologyTranslation'; +import { getPropertyDescription } from '@topology-utils/swagger-utils'; +import { K8sResourceKind } from '@topology-utils/types/k8s-types'; + +import EditButton from './components/EditButton'; +import LinkifyExternal from './components/LinkifyExternal'; +import PropertyPath from './components/PropertyPath'; + +export type DetailsItemProps = { + canEdit?: boolean; + defaultValue?: ReactNode; + description?: string; + editAsGroup?: boolean; + hideEmpty?: boolean; + label: string; + labelClassName?: string; + obj?: K8sResourceKind; + onEdit?: (e: MouseEvent) => void; + path?: string | string[]; + valueClassName?: string; +}; + +const DetailsItem: FC = ({ + children, + defaultValue = '-', + description, + editAsGroup, + hideEmpty, + label, + labelClassName, + obj, + onEdit, + canEdit = true, + path, + valueClassName, +}) => { + const { t } = useTopologyTranslation(); + const model = getK8sModel(obj); + const hide = hideEmpty && isEmpty(get(obj, path)); + const popoverContent: string = description ?? getPropertyDescription(model, path); + const value: ReactNode = children || get(obj, path, defaultValue); + const editable = onEdit && canEdit; + return hide ? null : ( + <> +
+ + + {popoverContent || path ? ( + {label}} + {...(popoverContent && { + bodyContent: ( + +
{popoverContent}
+
+ ), + })} + {...(path && { footerContent: })} + maxWidth="30rem" + > + +
+ ) : ( + label + )} +
+ {editable && editAsGroup && ( + <> + + + + {t('Edit')} + + + + )} +
+
+
+ {editable && !editAsGroup ? ( + + {value} + + ) : ( + value + )} +
+ + ); +}; + +export default DetailsItem; diff --git a/src/components/common/DetailsItem/components/EditButton.tsx b/src/components/common/DetailsItem/components/EditButton.tsx new file mode 100644 index 0000000..36a7798 --- /dev/null +++ b/src/components/common/DetailsItem/components/EditButton.tsx @@ -0,0 +1,28 @@ +import React, { MouseEvent, SFC } from 'react'; + +import { Button } from '@patternfly/react-core'; +import { PencilAltIcon } from '@patternfly/react-icons'; + +type EditButtonProps = { + onClick: (e: MouseEvent) => void; + testId?: string; +}; + +const EditButton: SFC = (props) => { + return ( + + ); +}; + +export default EditButton; diff --git a/src/components/common/DetailsItem/components/LinkifyExternal.tsx b/src/components/common/DetailsItem/components/LinkifyExternal.tsx new file mode 100644 index 0000000..46391ff --- /dev/null +++ b/src/components/common/DetailsItem/components/LinkifyExternal.tsx @@ -0,0 +1,8 @@ +import React, { FC, ReactNode } from 'react'; +import Linkify from 'react-linkify'; + +const LinkifyExternal: FC<{ children: ReactNode }> = ({ children }) => ( + {children} +); + +export default LinkifyExternal; diff --git a/src/components/common/DetailsItem/components/PropertyPath.tsx b/src/components/common/DetailsItem/components/PropertyPath.tsx new file mode 100644 index 0000000..f5128b8 --- /dev/null +++ b/src/components/common/DetailsItem/components/PropertyPath.tsx @@ -0,0 +1,23 @@ +import React, { FC } from 'react'; +import toPath from 'lodash.topath'; + +import { Breadcrumb, BreadcrumbItem } from '@patternfly/react-core'; + +const PropertyPath: FC<{ kind: string; path: string | string[] }> = ({ kind, path }) => { + const pathArray: string[] = toPath(path); + return ( + + {kind} + {pathArray.map((property, i) => { + const isLast = i === pathArray.length - 1; + return ( + + {property} + + ); + })} + + ); +}; + +export default PropertyPath; diff --git a/src/components/common/ExternalLink.tsx b/src/components/common/ExternalLink.tsx new file mode 100644 index 0000000..2821929 --- /dev/null +++ b/src/components/common/ExternalLink.tsx @@ -0,0 +1,32 @@ +import React, { FC, ReactNode } from 'react'; +import classNames from 'classnames'; + +type ExternalLinkProps = { + href: string; + text?: ReactNode; + additionalClassName?: string; + dataTestID?: string; + stopPropagation?: boolean; +}; + +const ExternalLink: FC = ({ + children, + href, + text, + additionalClassName = '', + dataTestID, + stopPropagation, +}) => ( + e.stopPropagation() } : {})} + > + {children || text} + +); + +export default ExternalLink; diff --git a/src/components/common/LabelList/LabelList.tsx b/src/components/common/LabelList/LabelList.tsx new file mode 100644 index 0000000..028d7c6 --- /dev/null +++ b/src/components/common/LabelList/LabelList.tsx @@ -0,0 +1,49 @@ +import React, { Component } from 'react'; +import { WithTranslation, withTranslation } from 'react-i18next'; +import isEqual from 'lodash.isequal'; + +import { K8sResourceKindReference } from '@openshift-console/dynamic-plugin-sdk'; +import { LabelGroup as PfLabelGroup } from '@patternfly/react-core'; +import { isEmpty } from '@topology-utils/common-utils'; + +import Label from './components/Label'; + +export type LabelListProps = WithTranslation & { + labels: { [key: string]: string }; + kind: K8sResourceKindReference; + expand?: boolean; +}; + +class TranslatedLabelList extends Component { + shouldComponentUpdate(nextProps) { + return !isEqual(nextProps, this.props); + } + + render() { + const { labels, kind, t, expand = true } = this.props; + const list = Object.entries(labels)?.map(([key, label]) => ( +