From 718cd5ecbb6a89d64021c7e2fded9bc719c898fd Mon Sep 17 00:00:00 2001 From: Radim Hrazdil Date: Mon, 17 Dec 2018 14:07:23 +0100 Subject: [PATCH 01/24] Adding junitReporter to generate xml report --- frontend/integration-tests/protractor.conf.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/integration-tests/protractor.conf.ts b/frontend/integration-tests/protractor.conf.ts index 078893f3934..0d588085cf9 100644 --- a/frontend/integration-tests/protractor.conf.ts +++ b/frontend/integration-tests/protractor.conf.ts @@ -4,7 +4,7 @@ import { Config, browser, logging } from 'protractor'; import { execSync } from 'child_process'; import * as HtmlScreenshotReporter from 'protractor-jasmine2-screenshot-reporter'; import * as _ from 'lodash'; -import { TapReporter } from 'jasmine-reporters'; +import { TapReporter, JUnitXmlReporter } from 'jasmine-reporters'; import * as ConsoleReporter from 'jasmine-console-reporter'; import * as failFast from 'protractor-fail-fast'; @@ -14,6 +14,7 @@ export const appHost = `${process.env.BRIDGE_BASE_ADDRESS || 'http://localhost:9 export const testName = `test-${Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 5)}`; const htmlReporter = new HtmlScreenshotReporter({dest: './gui_test_screenshots', inlineImages: true, captureOnlyFailedSpecs: true, filename: 'test-gui-report.html'}); +const junitReporter = new JUnitXmlReporter({savePath: './gui_test_screenshots', consolidateAll: true}); const browserLogs: logging.Entry[] = []; export const config: Config = { @@ -49,6 +50,7 @@ export const config: Config = { onPrepare: () => { browser.waitForAngularEnabled(false); jasmine.getEnv().addReporter(htmlReporter); + jasmine.getEnv().addReporter(junitReporter); if (tap) { jasmine.getEnv().addReporter(new TapReporter()); } else { From e02ba69cbfeb0bdfc0040e3b02199f54301faa63 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Tue, 15 Jan 2019 12:15:24 -0500 Subject: [PATCH 02/24] Add Dev Catalog tests to Jenkins CI --- frontend/integration-tests/protractor.conf.ts | 5 ++- ...nario.ts => developer-catalog.scenario.ts} | 32 ++++++++++++------- .../components/catalog/catalog-page.jsx | 9 +++--- 3 files changed, 27 insertions(+), 19 deletions(-) rename frontend/integration-tests/tests/{catalog.scenario.ts => developer-catalog.scenario.ts} (71%) diff --git a/frontend/integration-tests/protractor.conf.ts b/frontend/integration-tests/protractor.conf.ts index ecb69077008..677714233aa 100644 --- a/frontend/integration-tests/protractor.conf.ts +++ b/frontend/integration-tests/protractor.conf.ts @@ -95,8 +95,7 @@ export const config: Config = { olm: ['tests/base.scenario.ts', 'tests/olm/descriptors.scenario.ts', 'tests/olm/catalog.scenario.ts', 'tests/olm/etcd.scenario.ts'], olmUpgrade: ['tests/base.scenario.ts', 'tests/olm/update-channel-approval.scenario.ts'], performance: ['tests/base.scenario.ts', 'tests/performance.scenario.ts'], - serviceCatalog: ['tests/base.scenario.ts', 'tests/service-catalog/service-catalog.scenario.ts', 'tests/service-catalog/service-broker.scenario.ts', 'tests/service-catalog/service-class.scenario.ts', 'tests/service-catalog/service-binding.scenario.ts'], - catalog: ['tests/base.scenario.ts', 'tests/catalog.scenario.ts'], + serviceCatalog: ['tests/base.scenario.ts', 'tests/service-catalog/service-catalog.scenario.ts', 'tests/service-catalog/service-broker.scenario.ts', 'tests/service-catalog/service-class.scenario.ts', 'tests/service-catalog/service-binding.scenario.ts', 'tests/developer-catalog.scenario.ts'], operatorHub: ['tests/base.scenario.ts', 'tests/operator-hub/operator-hub.scenario.ts'], overview: ['tests/base.scenario.ts', 'tests/overview/overview.scenario.ts'], e2e: ['tests/base.scenario.ts', @@ -123,7 +122,7 @@ export const config: Config = { 'tests/source-to-image.scenario.ts', 'tests/deploy-image.scenario.ts', 'tests/operator-hub/operator-hub.scenario.ts', - 'tests/catalog.scenario.ts'], + 'tests/developer-catalog.scenario.ts'], }, params: { // Set to 'true' to enable OpenShift resources in the crud scenario. diff --git a/frontend/integration-tests/tests/catalog.scenario.ts b/frontend/integration-tests/tests/developer-catalog.scenario.ts similarity index 71% rename from frontend/integration-tests/tests/catalog.scenario.ts rename to frontend/integration-tests/tests/developer-catalog.scenario.ts index 74e8cf12bc3..eeba3103180 100644 --- a/frontend/integration-tests/tests/catalog.scenario.ts +++ b/frontend/integration-tests/tests/developer-catalog.scenario.ts @@ -40,9 +40,9 @@ describe('Catalog', () => { expect(numLanguagesItems).toBeLessThan(origNumItems); }); - it('displays ".NET Core" catalog tile when filter by name: "net"', async() => { - await catalogPageView.filterByKeyword('net'); - expect(catalogPageView.catalogTileFor('.NET Core').isDisplayed()).toBe(true); + it('displays "Jenkins" catalog tile when filter by name: "jenkins"', async() => { + await catalogPageView.filterByKeyword('jenkins'); + expect(catalogPageView.catalogTileFor('Jenkins').isDisplayed()).toBe(true); }); it('displays "No Filter Results" page correctly', async() => { @@ -60,28 +60,36 @@ describe('Catalog', () => { expect(catalogPageView.catalogTiles.isPresent()).toBe(true); const srvClassFilterCount = await catalogPageView.filterCheckboxCount('Service Class'); - // '.NET Core' is source-to-image - expect(catalogPageView.catalogTileFor('.NET Core').isDisplayed()).toBe(true); + + // 'Node.js' is source-to-image and should be shown initially + expect(catalogPageView.catalogTileFor('Node.js').isDisplayed()).toBe(true); await catalogPageView.clickFilterCheckbox('Service Class'); + // 'Jenkins' is service-class and should be shown + expect(catalogPageView.catalogTileFor('Jenkins').isDisplayed()).toBe(true); + // 'Node.js' is s-2-i and should not be shown + expect(catalogPageView.catalogTileFor('Node.js').isPresent()).toBe(false); + const numCatalogTiles = await catalogView.pageHeadingNumberOfItems(); - // after checking '[X] Service Class (12)', the number of tiles should equal the 'Service Class' filter count + // after checking '[X] Service Class', the number of tiles should equal the 'Service Class' filter count expect(srvClassFilterCount).toEqual(numCatalogTiles); - // // '.NET Core' is s-t-i and should not be shown - expect(catalogPageView.catalogTileFor('.NET Core').isPresent()).toBe(false); }); it('filters catalog tiles by \'Source-To-Image\' Type correctly', async() => { expect(catalogPageView.catalogTiles.isPresent()).toBe(true); const srvClassFilterCount = await catalogPageView.filterCheckboxCount('Source-To-Image'); - // '.NET Core Example' is service class - expect(catalogPageView.catalogTileFor('.NET Core Example').isDisplayed()).toBe(true); + + // 'Jenkins' is service class and should be shown initially + expect(catalogPageView.catalogTileFor('Jenkins').isDisplayed()).toBe(true); await catalogPageView.clickFilterCheckbox('Source-To-Image'); + // 'Node.js' is s-2-i and should be shown + expect(catalogPageView.catalogTileFor('Node.js').isPresent()).toBe(true); + // 'Jenkins' is service-class and should not be shown + expect(catalogPageView.catalogTileFor('Jenkins').isPresent()).toBe(false); + const numCatalogTiles = await catalogView.pageHeadingNumberOfItems(); expect(srvClassFilterCount).toEqual(numCatalogTiles); - // // '.NET Core Example' is service-class and should not be shown - expect(catalogPageView.catalogTileFor('.NET Core Example').isPresent()).toBe(false); }); }); diff --git a/frontend/public/components/catalog/catalog-page.jsx b/frontend/public/components/catalog/catalog-page.jsx index 4e3e22ad2cf..f4a2030dbd6 100644 --- a/frontend/public/components/catalog/catalog-page.jsx +++ b/frontend/public/components/catalog/catalog-page.jsx @@ -32,10 +32,11 @@ export class CatalogListPage extends React.Component { } componentDidUpdate(prevProps) { - const {clusterserviceclasses, imagestreams, namespace} = this.props; - if (namespace !== prevProps.namespace || - clusterserviceclasses !== prevProps.clusterserviceclasses || - imagestreams !== prevProps.imagestreams) { + const {clusterserviceclasses, imagestreams, clusterServiceVersions, namespace} = this.props; + if (!_.isEqual(namespace, prevProps.namespace) || + !_.isEqual(clusterserviceclasses, prevProps.clusterserviceclasses) || + !_.isEqual(imagestreams, prevProps.imagestreams) || + !_.isEqual(clusterServiceVersions, prevProps.clusterServiceVersions)) { const items = this.getItems(); this.setState({items}); } From a361219dee222c3f2e78801ce542f54de6e38961 Mon Sep 17 00:00:00 2001 From: Robb Hamilton Date: Thu, 24 Jan 2019 14:20:39 -0500 Subject: [PATCH 03/24] Remove undesirable border at bottom of events page --- frontend/public/components/events.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/public/components/events.jsx b/frontend/public/components/events.jsx index eb8297f1c09..acaa225fcfa 100644 --- a/frontend/public/components/events.jsx +++ b/frontend/public/components/events.jsx @@ -465,7 +465,7 @@ class EventStream extends SafetyFirst { }); const messageCount = count < maxMessages ? `Showing ${pluralize(count, 'event')}` : `Showing ${count} of ${allCount}+ events`; - return
+ return
From 0b5c574841c183b7079a5a16edc7d6a42782e44c Mon Sep 17 00:00:00 2001 From: Samuel Padgett Date: Fri, 25 Jan 2019 14:58:28 -0500 Subject: [PATCH 04/24] Refresh metrics when changing selected overview project --- frontend/public/components/overview/index.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/frontend/public/components/overview/index.tsx b/frontend/public/components/overview/index.tsx index 763ef7e17e0..17b79e748a7 100644 --- a/frontend/public/components/overview/index.tsx +++ b/frontend/public/components/overview/index.tsx @@ -466,7 +466,7 @@ class OverviewMainContent_ extends React.Component { if (!prometheusBasePath) { // Component is not mounted or proxy has not been set up. return; From 7ccd37eaa255ac9b575a72a7c84b644c2c125bad Mon Sep 17 00:00:00 2001 From: Samuel Padgett Date: Wed, 5 Dec 2018 12:47:19 -0500 Subject: [PATCH 05/24] Update console for monitoring changes * Proxy to port 9092, which has the tenancy proxy in front of it * Remove the `CAN_LIST_NS` check since users can now see metrics in their own namespaces https://jira.coreos.com/browse/CONSOLE-1035 --- cmd/bridge/main.go | 8 +++ frontend/public/components/build.tsx | 8 ++- .../public/components/cluster-overview.jsx | 9 +-- frontend/public/components/graphs/base.jsx | 10 +-- frontend/public/components/graphs/index.jsx | 36 ++-------- frontend/public/components/namespace.jsx | 5 +- frontend/public/components/overview/index.tsx | 7 +- frontend/public/components/pod.jsx | 6 +- server/server.go | 71 +++++++++++-------- 9 files changed, 78 insertions(+), 82 deletions(-) diff --git a/cmd/bridge/main.go b/cmd/bridge/main.go index 909e72c5271..ce5f7243f42 100644 --- a/cmd/bridge/main.go +++ b/cmd/bridge/main.go @@ -31,6 +31,9 @@ const ( // Well-known location of Prometheus service for OpenShift. This is only accessible in-cluster. openshiftPrometheusHost = "prometheus-k8s.openshift-monitoring.svc:9091" + // The tenancy service port (9092) performs RBAC checks for namespace-specific queries. + openshiftPrometheusTenancyHost = "prometheus-k8s.openshift-monitoring.svc:9092" + // Well-known location of Alert Manager service for OpenShift. This is only accessible in-cluster. openshiftAlertManagerHost = "alertmanager-main.openshift-monitoring.svc:9094" ) @@ -262,6 +265,11 @@ func main() { HeaderBlacklist: []string{"Cookie", "X-CSRFToken"}, Endpoint: &url.URL{Scheme: "https", Host: openshiftPrometheusHost, Path: "/api"}, } + srv.PrometheusTenancyProxyConfig = &proxy.Config{ + TLSClientConfig: monitoringProxyTLSConfig, + HeaderBlacklist: []string{"Cookie", "X-CSRFToken"}, + Endpoint: &url.URL{Scheme: "https", Host: openshiftPrometheusTenancyHost, Path: "/api"}, + } srv.AlertManagerProxyConfig = &proxy.Config{ TLSClientConfig: monitoringProxyTLSConfig, HeaderBlacklist: []string{"Cookie", "X-CSRFToken"}, diff --git a/frontend/public/components/build.tsx b/frontend/public/components/build.tsx index bf934d38992..ef93c26315b 100644 --- a/frontend/public/components/build.tsx +++ b/frontend/public/components/build.tsx @@ -87,16 +87,18 @@ const BuildGraphs = requirePrometheus(({build}) => { return null; } + const namespace = build.metadata.namespace; + return
- +
- +
- +
diff --git a/frontend/public/components/cluster-overview.jsx b/frontend/public/components/cluster-overview.jsx index fcad9c87b60..eb7b876b0ba 100644 --- a/frontend/public/components/cluster-overview.jsx +++ b/frontend/public/components/cluster-overview.jsx @@ -2,7 +2,6 @@ import * as _ from 'lodash-es'; import * as React from 'react'; import * as classNames from 'classnames'; import { Helmet } from 'react-helmet'; -import { Link } from 'react-router-dom'; import { EventsList } from './events'; import { SoftwareDetails } from './software-details'; @@ -20,15 +19,11 @@ import { StatusBox, } from './utils'; - -const DashboardLink = ({to, id}) => View Grafana Dashboard; - -const Graphs = requirePrometheus(({namespace, isOpenShift}) => { +const Graphs = requirePrometheus(({namespace}) => { return

Health

- {!isOpenShift && }
@@ -38,7 +33,6 @@ const Graphs = requirePrometheus(({namespace, isOpenShift}) => {

Control Plane Status

- {!isOpenShift && }
@@ -62,7 +56,6 @@ const Graphs = requirePrometheus(({namespace, isOpenShift}) => {

Capacity Planning

- {!isOpenShift && }
diff --git a/frontend/public/components/graphs/base.jsx b/frontend/public/components/graphs/base.jsx index 0a62e62cf60..6c401051c7e 100644 --- a/frontend/public/components/graphs/base.jsx +++ b/frontend/public/components/graphs/base.jsx @@ -7,7 +7,7 @@ import * as classNames from 'classnames'; import { coFetchJSON } from '../../co-fetch'; import { SafetyFirst } from '../safety-first'; -import { prometheusBasePath } from './index'; +import { prometheusBasePath, prometheusTenancyBasePath } from './index'; import { MonitoringRoutes } from '../../monitoring'; export class BaseGraph extends SafetyFirst { @@ -44,13 +44,14 @@ export class BaseGraph extends SafetyFirst { }]; } - const basePath = this.props.basePath || prometheusBasePath; + const basePath = this.props.basePath || (this.props.namespace ? prometheusTenancyBasePath : prometheusBasePath); const pollInterval = timeSpan / 120 || 15000; const stepSize = pollInterval / 1000; const promises = queries.map(q => { + const nsParam = this.props.namespace ? `&namespace=${encodeURIComponent(this.props.namespace)}` : ''; const url = this.timeSpan - ? `${basePath}/api/v1/query_range?query=${encodeURIComponent(q.query)}&start=${start / 1000}&end=${end / 1000}&step=${stepSize}` - : `${basePath}/api/v1/query?query=${encodeURIComponent(q.query)}`; + ? `${basePath}/api/v1/query_range?query=${encodeURIComponent(q.query)}&start=${start / 1000}&end=${end / 1000}&step=${stepSize}${nsParam}` + : `${basePath}/api/v1/query?query=${encodeURIComponent(q.query)}${nsParam}`; return coFetchJSON(url); }); Promise.all(promises) @@ -142,6 +143,7 @@ export class BaseGraph extends SafetyFirst { } BaseGraph.propTypes = { + namespace: PropTypes.string, query: PropTypes.oneOfType([ PropTypes.string, PropTypes.arrayOf( diff --git a/frontend/public/components/graphs/index.jsx b/frontend/public/components/graphs/index.jsx index f7451765440..6ace8d1623d 100644 --- a/frontend/public/components/graphs/index.jsx +++ b/frontend/public/components/graphs/index.jsx @@ -2,10 +2,11 @@ import * as React from 'react'; import { AsyncComponent } from '../utils/async'; -import { FLAGS, connectToFlags, flagPending } from '../../features'; +import { FLAGS, connectToFlags } from '../../features'; export { Status, errorStatus } from './status'; export const prometheusBasePath = window.SERVER_FLAGS.prometheusBaseURL; +export const prometheusTenancyBasePath = window.SERVER_FLAGS.prometheusTenancyBaseURL; export const alertManagerBasePath = window.SERVER_FLAGS.alertManagerBaseURL; export const Bar = props => import('./graph-loader').then(c => c.Bar)} {...props} />; @@ -14,39 +15,16 @@ export const Line = props => import('./graph-loade export const Scalar = props => import('./graph-loader').then(c => c.Scalar)} {...props} />; export const Donut = props => import('./graph-loader').then(c => c.Donut)} {...props} />; -const canAccessPrometheus = (openshiftFlag, prometheusFlag, canListNS) => { - if (flagPending(prometheusFlag) || flagPending(openshiftFlag)) { - // Wait for feature detection to complete before showing graphs so we don't show them, then hide them. - return false; - } - - if (!prometheusFlag) { - return false; - } - - if (!window.SERVER_FLAGS.prometheusBaseURL) { - // Proxy has not been set up. - return false; - } - - if (!openshiftFlag) { - return true; - } - - // In OpenShift, the user must be able to list namespaces to query Prometheus. - return canListNS; -}; +const canAccessPrometheus = (prometheusFlag) => prometheusFlag && !!prometheusBasePath && !!prometheusTenancyBasePath; // HOC that will hide WrappedComponent when Prometheus isn't configured or the user doesn't have permission to query Prometheus. -/** @type {(WrappedComponent: React.SFC

) => React.ComponentType

} */ -export const requirePrometheus = WrappedComponent => connectToFlags(FLAGS.OPENSHIFT, FLAGS.PROMETHEUS, FLAGS.CAN_LIST_NS)(props => { +/** @type {(WrappedComponent: React.SFC

) => React.ComponentType

} */ +export const requirePrometheus = WrappedComponent => connectToFlags(FLAGS.PROMETHEUS)(props => { const { flags } = props; - const openshiftFlag = flags[FLAGS.OPENSHIFT]; const prometheusFlag = flags[FLAGS.PROMETHEUS]; - const canListNS = flags[FLAGS.CAN_LIST_NS]; - if (!canAccessPrometheus(openshiftFlag, prometheusFlag, canListNS)) { + if (!canAccessPrometheus(prometheusFlag)) { return null; } - return ; + return ; }); diff --git a/frontend/public/components/namespace.jsx b/frontend/public/components/namespace.jsx index 6371b783bee..05df4984a34 100644 --- a/frontend/public/components/namespace.jsx +++ b/frontend/public/components/namespace.jsx @@ -164,7 +164,7 @@ class PullSecret extends SafetyFirst { export const NamespaceLineCharts = ({ns}) =>

-
]} />
-
export const TopPodsBarChart = ({ns}) => ( diff --git a/frontend/public/components/overview/index.tsx b/frontend/public/components/overview/index.tsx index 17b79e748a7..386d4f80631 100644 --- a/frontend/public/components/overview/index.tsx +++ b/frontend/public/components/overview/index.tsx @@ -12,7 +12,7 @@ import { Toolbar, EmptyState } from 'patternfly-react'; import { coFetchJSON } from '../../co-fetch'; import { getBuildNumber } from '../../module/k8s/builds'; -import { prometheusBasePath } from '../graphs'; +import { prometheusTenancyBasePath } from '../graphs'; import { TextFilter } from '../factory'; import { UIActions, formatNamespacedRouteForResource } from '../../ui/ui-actions'; import { @@ -504,8 +504,7 @@ class OverviewMainContent_ extends React.Component { - if (!prometheusBasePath) { - // Component is not mounted or proxy has not been set up. + if (!prometheusTenancyBasePath) { return; } @@ -516,7 +515,7 @@ class OverviewMainContent_ extends React.Component { - const url = `${prometheusBasePath}/api/v1/query?query=${encodeURIComponent(query)}`; + const url = `${prometheusTenancyBasePath}/api/v1/query?namespace=${namespace}&query=${encodeURIComponent(query)}`; return coFetchJSON(url).then(({ data: {result} }) => { const byPod: MetricValuesByPod = result.reduce((acc, { metric, value }) => { acc[metric.pod_name] = Number(value[1]); diff --git a/frontend/public/components/pod.jsx b/frontend/public/components/pod.jsx index 4c6431e69d2..1f56d74f65c 100644 --- a/frontend/public/components/pod.jsx +++ b/frontend/public/components/pod.jsx @@ -133,13 +133,13 @@ export const PodContainerTable = ({heading, containers, pod}) =>
- +
- +
- +
diff --git a/server/server.go b/server/server.go index 4c26ee1a5fc..24a3d7d7229 100644 --- a/server/server.go +++ b/server/server.go @@ -23,14 +23,15 @@ const ( indexPageTemplateName = "index.html" tokenizerPageTemplateName = "tokener.html" - authLoginEndpoint = "/auth/login" - AuthLoginCallbackEndpoint = "/auth/callback" - AuthLoginSuccessEndpoint = "/" - AuthLoginErrorEndpoint = "/error" - authLogoutEndpoint = "/auth/logout" - k8sProxyEndpoint = "/api/kubernetes/" - prometheusProxyEndpoint = "/api/prometheus" - alertManagerProxyEndpoint = "/api/alertmanager" + authLoginEndpoint = "/auth/login" + AuthLoginCallbackEndpoint = "/auth/callback" + AuthLoginSuccessEndpoint = "/" + AuthLoginErrorEndpoint = "/error" + authLogoutEndpoint = "/auth/logout" + k8sProxyEndpoint = "/api/kubernetes/" + prometheusProxyEndpoint = "/api/prometheus" + prometheusTenancyProxyEndpoint = "/api/prometheus-tenancy" + alertManagerProxyEndpoint = "/api/alertmanager" ) var ( @@ -38,23 +39,24 @@ var ( ) type jsGlobals struct { - ConsoleVersion string `json:"consoleVersion"` - AuthDisabled bool `json:"authDisabled"` - KubectlClientID string `json:"kubectlClientID"` - BasePath string `json:"basePath"` - LoginURL string `json:"loginURL"` - LoginSuccessURL string `json:"loginSuccessURL"` - LoginErrorURL string `json:"loginErrorURL"` - LogoutURL string `json:"logoutURL"` - LogoutRedirect string `json:"logoutRedirect"` - KubeAPIServerURL string `json:"kubeAPIServerURL"` - PrometheusBaseURL string `json:"prometheusBaseURL"` - AlertManagerBaseURL string `json:"alertManagerBaseURL"` - Branding string `json:"branding"` - DocumentationBaseURL string `json:"documentationBaseURL"` - ClusterName string `json:"clusterName"` - GoogleTagManagerID string `json:"googleTagManagerID"` - LoadTestFactor int `json:"loadTestFactor"` + ConsoleVersion string `json:"consoleVersion"` + AuthDisabled bool `json:"authDisabled"` + KubectlClientID string `json:"kubectlClientID"` + BasePath string `json:"basePath"` + LoginURL string `json:"loginURL"` + LoginSuccessURL string `json:"loginSuccessURL"` + LoginErrorURL string `json:"loginErrorURL"` + LogoutURL string `json:"logoutURL"` + LogoutRedirect string `json:"logoutRedirect"` + KubeAPIServerURL string `json:"kubeAPIServerURL"` + PrometheusBaseURL string `json:"prometheusBaseURL"` + PrometheusTenancyBaseURL string `json:"prometheusTenancyBaseURL"` + AlertManagerBaseURL string `json:"alertManagerBaseURL"` + Branding string `json:"branding"` + DocumentationBaseURL string `json:"documentationBaseURL"` + ClusterName string `json:"clusterName"` + GoogleTagManagerID string `json:"googleTagManagerID"` + LoadTestFactor int `json:"loadTestFactor"` } type Server struct { @@ -75,9 +77,10 @@ type Server struct { LoadTestFactor int DexClient api.DexClient // A client with the correct TLS setup for communicating with the API server. - K8sClient *http.Client - PrometheusProxyConfig *proxy.Config - AlertManagerProxyConfig *proxy.Config + K8sClient *http.Client + PrometheusProxyConfig *proxy.Config + PrometheusTenancyProxyConfig *proxy.Config + AlertManagerProxyConfig *proxy.Config } func (s *Server) authDisabled() bool { @@ -85,7 +88,7 @@ func (s *Server) authDisabled() bool { } func (s *Server) prometheusProxyEnabled() bool { - return s.PrometheusProxyConfig != nil + return s.PrometheusProxyConfig != nil && s.PrometheusTenancyProxyConfig != nil } func (s *Server) alertManagerProxyEnabled() bool { @@ -189,6 +192,15 @@ func (s *Server) HTTPHandler() http.Handler { prometheusProxy.ServeHTTP(w, r) })), ) + prometheusTenancyProxyAPIPath := prometheusTenancyProxyEndpoint + "/api/" + prometheusTenancyProxy := proxy.NewProxy(s.PrometheusTenancyProxyConfig) + handle(prometheusTenancyProxyAPIPath, http.StripPrefix( + proxy.SingleJoiningSlash(s.BaseURL.Path, prometheusTenancyProxyAPIPath), + authHandlerWithUser(func(user *auth.User, w http.ResponseWriter, r *http.Request) { + r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", user.Token)) + prometheusTenancyProxy.ServeHTTP(w, r) + })), + ) } if s.alertManagerProxyEnabled() { @@ -251,6 +263,7 @@ func (s *Server) indexHandler(w http.ResponseWriter, r *http.Request) { if s.prometheusProxyEnabled() { jsg.PrometheusBaseURL = proxy.SingleJoiningSlash(s.BaseURL.Path, prometheusProxyEndpoint) + jsg.PrometheusTenancyBaseURL = proxy.SingleJoiningSlash(s.BaseURL.Path, prometheusTenancyProxyEndpoint) } if s.alertManagerProxyEnabled() { From 912df82a57381d86bf79ccc915893e74409fe0f4 Mon Sep 17 00:00:00 2001 From: Robb Hamilton Date: Mon, 28 Jan 2019 11:23:53 -0500 Subject: [PATCH 06/24] Fix bug where delete modal layout was broken --- .../public/components/modals/delete-modal.jsx | 26 ++++++++++--------- .../modals/delete-namespace-modal.jsx | 14 +++++----- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/frontend/public/components/modals/delete-modal.jsx b/frontend/public/components/modals/delete-modal.jsx index 4754d9c1a7b..e4bf001f3ce 100644 --- a/frontend/public/components/modals/delete-modal.jsx +++ b/frontend/public/components/modals/delete-modal.jsx @@ -45,18 +45,20 @@ class DeleteModal extends PromiseComponent { const {kind, resource} = this.props; return
Delete {kind.label} - - -
-

Delete {resource.metadata.name}?

-
Are you sure you want to delete {resource.metadata.name} - {_.has(resource.metadata, 'namespace') && in namespace { resource.metadata.namespace }?} - {_.has(kind, 'propagationPolicy') &&
- -
} + +
+ +
+

Delete {resource.metadata.name}?

+
Are you sure you want to delete {resource.metadata.name} + {_.has(resource.metadata, 'namespace') && in namespace { resource.metadata.namespace }?} + {_.has(kind, 'propagationPolicy') &&
+ +
} +
diff --git a/frontend/public/components/modals/delete-namespace-modal.jsx b/frontend/public/components/modals/delete-namespace-modal.jsx index 9ff259c9317..09b480accaf 100644 --- a/frontend/public/components/modals/delete-namespace-modal.jsx +++ b/frontend/public/components/modals/delete-namespace-modal.jsx @@ -30,12 +30,14 @@ class DeleteNamespaceModal extends PromiseComponent { render() { return Delete {this.props.kind.label} - - -
-

This action cannot be undone. It will destroy all pods, services and other objects in the deleted namespace.

-

Confirm deletion by typing {this.props.resource.metadata.name} below:

- + +
+ +
+

This action cannot be undone. It will destroy all pods, services and other objects in the deleted namespace.

+

Confirm deletion by typing {this.props.resource.metadata.name} below:

+ +
From 936cc588fcc576fa3e4e3ac1769e47c6c63c42aa Mon Sep 17 00:00:00 2001 From: Jakub Hadvig Date: Mon, 28 Jan 2019 00:47:19 +0100 Subject: [PATCH 07/24] Update engine check to allow node 11 --- frontend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/package.json b/frontend/package.json index 7b5a2ad12f4..d64c8e83170 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -153,6 +153,6 @@ "webpack-cli": "^2.0.12" }, "engines": { - "node": ">=8.x <=10.x" + "node": ">=8.x" } } From c66b96e0a95482a2ef6db3ff1837b6704cdc9668 Mon Sep 17 00:00:00 2001 From: Joseph Caiani Date: Thu, 24 Jan 2019 14:02:55 -0500 Subject: [PATCH 08/24] Combine all scripts to single e2e test script --- test-prow-e2e.sh | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100755 test-prow-e2e.sh diff --git a/test-prow-e2e.sh b/test-prow-e2e.sh new file mode 100755 index 00000000000..a0589db7cc6 --- /dev/null +++ b/test-prow-e2e.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +set -exuo pipefail + +ARTIFACT_DIR=/tmp/artifacts +export ARTIFACT_DIR + +./build.sh + +oc login -u kubeadmin -p $(cat "${ARTIFACT_DIR}/installer/auth/kubeadmin-password") + +source ./contrib/oc-environment.sh + +kubectl create -f https://raw.githubusercontent.com/operator-framework/operator-lifecycle-manager/master/deploy/okd/manifests/0.8.0/0000_30_06-rh-operators.configmap.yaml +kubectl create -f https://raw.githubusercontent.com/operator-framework/operator-lifecycle-manager/master/deploy/okd/manifests/0.8.0/0000_30_09-rh-operators.catalogsource.yaml + +./test-gui.sh e2e + +cp -rv ./frontend/gui_test_screenshots "${ARTIFACT_DIR}/gui_test_screenshots" From f7b32a56a2cbc70d8f475acdab30e0912b7fa3af Mon Sep 17 00:00:00 2001 From: Samuel Padgett Date: Mon, 28 Jan 2019 14:54:52 -0500 Subject: [PATCH 09/24] Fix synthetic event warnings We were unintentionally passing the event object to the modal as props. --- frontend/public/components/namespace.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/public/components/namespace.jsx b/frontend/public/components/namespace.jsx index 05df4984a34..7ab1b7ac9dd 100644 --- a/frontend/public/components/namespace.jsx +++ b/frontend/public/components/namespace.jsx @@ -67,7 +67,7 @@ const NamespaceRow = ({obj: ns}) => ; export const NamespacesList = props => ; -export const NamespacesPage = props => ; +export const NamespacesPage = props => createNamespaceModal({})} />; const projectMenuActions = [Kebab.factory.Edit, deleteModal]; @@ -120,7 +120,7 @@ export const ProjectList = connect(createProjectMessageStateToProps)(ProjectList const ProjectsPage_ = props => { const canCreate = props.flags.CAN_CREATE_PROJECT; - return ; + return createProjectModal({})} />; }; export const ProjectsPage = connectToFlags(FLAGS.CAN_CREATE_PROJECT)(ProjectsPage_); From 089218c0246318a097ecadef25eb047c17f28101 Mon Sep 17 00:00:00 2001 From: Joseph Caiani Date: Tue, 29 Jan 2019 13:41:07 -0500 Subject: [PATCH 10/24] Remove unused Jenkins and e2e scripts --- README.md | 15 ----- jenkins | 4 -- jenkins.sh | 153 ------------------------------------------ test-gui-e2e-setup.sh | 15 ----- 4 files changed, 187 deletions(-) delete mode 100755 jenkins delete mode 100755 jenkins.sh delete mode 100755 test-gui-e2e-setup.sh diff --git a/README.md b/README.md index 5b6b4b5725b..34c007b7b2a 100644 --- a/README.md +++ b/README.md @@ -137,21 +137,6 @@ Must set env vars `DOCKER_USER` and `DOCKER_PASSWORD` or have a valid `.dockercf ./build-docker-push.sh ``` -### Jenkins automation - -Master branch: - -* Runs a build, pushes an image to Quay tagged with the commit sha - -Pull requests: - -* Runs a build when PRs are created or PR commits are pushed -* Comment with `Jenkins rebuild` to manually trigger a re-build -* Comment with `Jenkins push` to push an image to Quay, tagged with: - `pr_[pr #]_build_[jenkins build #]` - -If changes are ever required for the Jenkins job configuration, apply them to both the [regular console job](https://jenkins-tectonic.prod.coreos.systems/job/console-build/) and [PR image job](https://jenkins-tectonic.prod.coreos.systems/job/console-pr-image/). - ## Hacking See [CONTRIBUTING](CONTRIBUTING.md) for workflow & convention details. diff --git a/jenkins b/jenkins deleted file mode 100755 index 6a5fbf91a63..00000000000 --- a/jenkins +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash - -# This is a transitional file. Remove it once Jenkins is updated and all old PRs are merged or closed -./jenkins.sh diff --git a/jenkins.sh b/jenkins.sh deleted file mode 100755 index 4ee5d22cf31..00000000000 --- a/jenkins.sh +++ /dev/null @@ -1,153 +0,0 @@ -#!/usr/bin/env bash - -set -eo pipefail - -# This script contains all jenkins work. -# This runs directly on the jenkins build host. -# The Jenkins build command should do nothing but execute this script. - -CURRENT_USER=$(whoami) -CURRENT_UID=$(id -u "$CURRENT_USER") -echo "Running under user: $CURRENT_USER, with uid: $CURRENT_UID" - -# We assume the jenkins jenkins user with uid 1000 on all build hosts -export BUILDER_RUN_USER=1000 - -if [ ${BUILDER_RUN_USER} -eq "${CURRENT_UID}" ]; then - echo "Running under User: ${CURRENT_USER}, with UID: ${CURRENT_UID}" -else - echo "Expected to run with UID: ${BUILDER_RUN_USER}, instead UID is: ${CURRENT_UID}. Fix Jenkins and try again." - exit 1 -fi - -S3_BUCKET="teamui-jenkins" -S3_URL="https://s3.amazonaws.com/$S3_BUCKET/" - -status() { - # Hide output so we don't leak creds in Jenkins log - set +x - description=${3:-"$1 $2."} - data=$(cat << EOF -{ - "context": "$1", - "state": "$2", - "description": "${description}", - "target_url": "${S3_URL}${BUILD_TAG}/${1}.log" -} -EOF -) - # TODO: use correct target url for performance status - # "target_url": "${BUILD_URL}console" - # shellcheck disable=SC2154 - curl -o /dev/null --silent -X POST --user "${GITHUB_CREDENTIALS}" \ - --data "$data" \ - "https://api.github.com/repos/openshift/console/statuses/${ghprbActualCommit}" - set -x -} - -s3_upload () { - # Hide output so we don't leak creds in Jenkins log - set +x - file=$1 - dest=$2 - content_type='text/plain' - datetime=$(TZ=utc date +"%a, %d %b %Y %T %z") - acl="x-amz-acl:public-read" - body="PUT\n\n${content_type}\n${datetime}\n${acl}\n/${S3_BUCKET}/${dest}" - signature=$(echo -en "${body}" | openssl sha1 -hmac "${AWS_SECRET_ACCESS_KEY}" -binary | base64) - - curl -o /dev/null --silent -X PUT -T "${file}" \ - -H "Host: ${S3_BUCKET}.s3.amazonaws.com" \ - -H "Date: ${datetime}" \ - -H "Content-Type: ${content_type}" \ - -H "${acl}" \ - -H "Authorization: AWS ${AWS_ACCESS_KEY_ID}:${signature}" \ - "https://${S3_BUCKET}.s3.amazonaws.com/${dest}" - set -x -} - - -builder_run () { - name=$1 - shift - needs_kubeconfig=$1 - shift - cmd=$* - - status "$name" 'pending' - - if [ "$needs_kubeconfig" -ne 0 ] - then - export DOCKER_ENV="KUBECONFIG" - fi - - mkdir -p jenkins-logs - # shellcheck disable=SC2086 - if ./builder-run.sh $cmd 2>&1 | tee "jenkins-logs/$name.log" - then - status "$name" 'success' - s3_upload "jenkins-logs/$name.log" "$BUILD_TAG/$name.log" - else - status "$name" 'error' - s3_upload "jenkins-logs/$name.log" "$BUILD_TAG/$name.log" - exit 1 - fi - unset DOCKER_ENV -} - -set -x -./clean.sh - -set +e - -builder_run 'Build' 0 ./build.sh -builder_run 'Tests' 0 ./test.sh -builder_run 'GUI-Tests' 1 ./test-gui.sh crud -builder_run 'GUI-Tests-New-App' 1 ./test-gui.sh newApp -builder_run 'GUI-Tests-OLM' 1 ./test-gui.sh olm -builder_run 'GUI-Tests-Service-Catalog' 1 ./test-gui.sh serviceCatalog - -status 'Performance' 'pending' -if DOCKER_ENV="KUBECONFIG" ./builder-run.sh ./test-gui.sh performance -then - description=$(cat ./frontend/gui_test_screenshots/bundle-analysis.txt) - status 'Performance' 'success' "${description}" -else - description=$(cat ./frontend/gui_test_screenshots/bundle-analysis.txt) - status 'Performance' 'error' "${description}" - exit 1 -fi - -set -e - -GIT_SHA_HEAD=$(git rev-parse HEAD) -GIT_SHA_MASTER=$(git rev-parse origin/master) -IS_RELEASE_TAG=$(git describe --exact-match --abbrev=0 --tags "${GIT_SHA_HEAD}" 2> /dev/null || :) -if [ "$GIT_SHA_HEAD" == "$GIT_SHA_MASTER" ]; then - echo "detected master build. building & pushing images..." - ./push.sh -elif [ ! -z "$IMAGE_TAG" ]; then - echo "detected request to push built image using tag ${IMAGE_TAG}. building & pushing images..." - ./push.sh -elif [ -n "$IS_RELEASE_TAG" ]; then - echo "detected release tag ${IS_RELEASE_TAG}. building & pushing images..." - ./push.sh -else - echo "skipping image push. HEAD sha does not appear to be master, nor is it a release tag: $GIT_SHA_HEAD" -fi - -echo "Cleaning up old Docker images..." - -set +e -# delete stopped containers -docker ps -a -q | xargs docker rm -# docker rm $(docker ps -a -q) - -# delete images except for console builder (fails on images currently used) -docker images | grep -F -v quay.io/coreos/tectonic-console-builder | awk '{print $3}' | grep -v IMAGE | xargs docker rmi - -# delete orphaned volumes -docker volume ls -qf dangling=true | xargs -r docker volume rm -set -e - -echo "Done!" diff --git a/test-gui-e2e-setup.sh b/test-gui-e2e-setup.sh deleted file mode 100755 index ff2db490113..00000000000 --- a/test-gui-e2e-setup.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash - -set -exuo pipefail - -ARTIFACT_DIR=/tmp/artifacts -export ARTIFACT_DIR - -./build.sh - -oc login -u kubeadmin -p $(cat "${ARTIFACT_DIR}/installer/auth/kubeadmin-password") - -source ./contrib/oc-environment.sh - -kubectl create -f https://raw.githubusercontent.com/operator-framework/operator-lifecycle-manager/master/deploy/okd/manifests/0.8.0/0000_30_06-rh-operators.configmap.yaml -kubectl create -f https://raw.githubusercontent.com/operator-framework/operator-lifecycle-manager/master/deploy/okd/manifests/0.8.0/0000_30_09-rh-operators.catalogsource.yaml From 610726a4a4d7f69bb5919821643a22735694793e Mon Sep 17 00:00:00 2001 From: Rebecca Alpert <7014965+rebeccaalpert@users.noreply.github.com> Date: Tue, 29 Jan 2019 14:06:41 -0500 Subject: [PATCH 11/24] frontend: Adjust placement of contextDir in buildConfig Moved contextDir from under git to source. ContextDir now shows up in the BuildConfig YAML. Fixes https://bugzilla.redhat.com/show_bug.cgi?id=1669974 --- frontend/public/components/source-to-image.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/public/components/source-to-image.tsx b/frontend/public/components/source-to-image.tsx index b265f034a7f..445780721af 100644 --- a/frontend/public/components/source-to-image.tsx +++ b/frontend/public/components/source-to-image.tsx @@ -219,10 +219,10 @@ class BuildSource extends React.Component { }, }, source: { + contextDir, git: { uri: repository, ref, - contextDir, type: 'Git', }, }, From 4f11a2e98aec868a63b5bc3fdbf3767b38da9bca Mon Sep 17 00:00:00 2001 From: Jon Jackson Date: Wed, 30 Jan 2019 08:28:27 -0500 Subject: [PATCH 12/24] Remove feature unavailable and mock code from cluster settings --- .../cluster-settings/cluster-settings.tsx | 71 ++++++------------- frontend/public/features.ts | 5 +- 2 files changed, 21 insertions(+), 55 deletions(-) diff --git a/frontend/public/components/cluster-settings/cluster-settings.tsx b/frontend/public/components/cluster-settings/cluster-settings.tsx index dce724ff568..fc325b028a3 100644 --- a/frontend/public/components/cluster-settings/cluster-settings.tsx +++ b/frontend/public/components/cluster-settings/cluster-settings.tsx @@ -21,23 +21,12 @@ enum ClusterUpdateStatus { Updating = 'Updating', Failing = 'Failing', ErrorRetrieving = 'Error Retrieving', - FeatureUnavailable = '-', } -// TODO - REMOVE MOCK CODE -const MOCK_CLUSTER_UPDATE = localStorage.getItem('MOCK_CLUSTER_UPDATE'); -// END MOCK CODE - const clusterAutoscalerReference = referenceForModel(ClusterAutoscalerModel); const clusterVersionReference = referenceForModel(ClusterVersionModel); export const getAvailableClusterUpdates = (cv) => { - // TODO - REMOVE MOCK CODE - if (MOCK_CLUSTER_UPDATE) { - return [{ version: '4.0.0', payload: 'openshift/release:v4.0.0' }]; - } - // END MOCK CODE - return _.get(cv, 'status.availableUpdates'); }; @@ -51,33 +40,24 @@ const launchUpdateModal = (cv) => { }; const getClusterUpdateStatus = (cv: K8sResourceKind): ClusterUpdateStatus => { - // TODO Remove this line and uncomment the rest of this function once update feature is available - return ClusterUpdateStatus.FeatureUnavailable; - - // TODO - REMOVE MOCK CODE - // if (MOCK_CLUSTER_UPDATE) { - // return ClusterUpdateStatus.UpdatesAvailable; - // } - // // END MOCK CODE - - // const conditions = _.get(cv, 'status.conditions', []); - // const isFailingCondition = _.find(conditions, { type: 'Failing', status: 'True' }); - // if (isFailingCondition) { - // return ClusterUpdateStatus.Failing; - // } - - // const retrievedUpdatesFailedCondition = _.find(conditions, { type: 'RetrievedUpdates', status: 'False' }); - // if (retrievedUpdatesFailedCondition) { - // return ClusterUpdateStatus.ErrorRetrieving; - // } - - // const isProgressingCondition = _.find(conditions, { type: 'Progressing', status: 'True' }); - // if (isProgressingCondition) { - // return ClusterUpdateStatus.Updating; - // } - - // const updates = _.get(cv, 'status.availableUpdates'); - // return _.isEmpty(updates) ? ClusterUpdateStatus.UpToDate : ClusterUpdateStatus.UpdatesAvailable; + const conditions = _.get(cv, 'status.conditions', []); + const isFailingCondition = _.find(conditions, { type: 'Failing', status: 'True' }); + if (isFailingCondition) { + return ClusterUpdateStatus.Failing; + } + + const retrievedUpdatesFailedCondition = _.find(conditions, { type: 'RetrievedUpdates', status: 'False' }); + if (retrievedUpdatesFailedCondition) { + return ClusterUpdateStatus.ErrorRetrieving; + } + + const isProgressingCondition = _.find(conditions, { type: 'Progressing', status: 'True' }); + if (isProgressingCondition) { + return ClusterUpdateStatus.Updating; + } + + const updates = _.get(cv, 'status.availableUpdates'); + return _.isEmpty(updates) ? ClusterUpdateStatus.UpToDate : ClusterUpdateStatus.UpdatesAvailable; }; const getIconClass = (status: ClusterUpdateStatus) => { @@ -111,11 +91,6 @@ const UpdatesAvailableAlert = ({cv}) =>
; -const FeatureUnavailableAlert = () =>
- - Cluster updates are not supported in beta 1. -
; - const UpdateStatus: React.SFC = ({cv}) => { const status = getClusterUpdateStatus(cv); const iconClass = getIconClass(status); @@ -144,19 +119,13 @@ const CurrentVersion: React.SFC = ({cv}) => { const ClusterVersionDetailsTable: React.SFC = ({obj: cv, autoscalers}) => { const conditions = _.get(cv, 'status.conditions', []); const status = getClusterUpdateStatus(cv); - - // TODO - REMOVE MOCK CODE - const disableErrors = MOCK_CLUSTER_UPDATE || status === ClusterUpdateStatus.FeatureUnavailable; - const retrievedUpdatesFailedCondition = !disableErrors && _.find(conditions, { type: 'RetrievedUpdates', status: 'False' }); - const isFailingCondition = !disableErrors && _.find(conditions, { type: 'Failing', status: 'True' }); - // END MOCK CODE - + const retrievedUpdatesFailedCondition = _.find(conditions, { type: 'RetrievedUpdates', status: 'False' }); + const isFailingCondition = _.find(conditions, { type: 'Failing', status: 'True' }); return
{ status === ClusterUpdateStatus.Updating && } { status === ClusterUpdateStatus.UpdatesAvailable && } - { status === ClusterUpdateStatus.FeatureUnavailable && } { isFailingCondition && } { retrievedUpdatesFailedCondition && }
diff --git a/frontend/public/features.ts b/frontend/public/features.ts index 507bd95ed32..28e185227f4 100644 --- a/frontend/public/features.ts +++ b/frontend/public/features.ts @@ -115,10 +115,7 @@ const detectClusterVersion = dispatch => coFetchJSON(clusterVersionPath) } const availableUpdates = _.get(clusterVersion, 'status.availableUpdates'); - - // TODO - REMOVE MOCK CODE - setFlag(dispatch, FLAGS.CLUSTER_UPDATES_AVAILABLE, localStorage.getItem('MOCK_CLUSTER_UPDATE') || !_.isEmpty(availableUpdates)); - // END MOCK CODE + setFlag(dispatch, FLAGS.CLUSTER_UPDATES_AVAILABLE, !_.isEmpty(availableUpdates)); }, err => { if (_.includes([403, 404], _.get(err, 'response.status'))) { From fda4be50856f9823383b4091504acabbf9eda7a8 Mon Sep 17 00:00:00 2001 From: Jeffrey Phillips Date: Wed, 30 Jan 2019 08:10:19 -0500 Subject: [PATCH 13/24] Prevent initial scroll on operator hub page fixes https://bugzilla.redhat.com/show_bug.cgi?id=1669303 also fixes small issues: filter by keyword include's the operator's name field always show both install state filters set the correct community operator support page link --- frontend/__mocks__/operatorHubItemsMocks.ts | 7 +++++++ .../components/operator-hub/operator-hub.spec.tsx | 6 +++--- .../operator-hub/operator-hub-items.tsx | 15 +++++++++++++++ .../public/components/utils/tile-view-page.jsx | 4 ++-- frontend/public/const.js | 3 +-- 5 files changed, 28 insertions(+), 7 deletions(-) diff --git a/frontend/__mocks__/operatorHubItemsMocks.ts b/frontend/__mocks__/operatorHubItemsMocks.ts index 85f64189444..a97146e0059 100644 --- a/frontend/__mocks__/operatorHubItemsMocks.ts +++ b/frontend/__mocks__/operatorHubItemsMocks.ts @@ -307,6 +307,7 @@ export const operatorHubTileViewPageProps = { items: [ { obj: amqPackageManifest, + installState: 'Installed', kind: 'PackageManifest', name: 'amq-streams', uid: 'amq-streams/openshift-operator-lifecycle-manager', @@ -327,6 +328,7 @@ export const operatorHubTileViewPageProps = { }, { obj: etcdPackageManifest, + installState: 'Not Installed', kind: 'PackageManifest', name: 'etcd', uid: 'etcd/openshift-operator-lifecycle-manager', @@ -346,6 +348,7 @@ export const operatorHubTileViewPageProps = { categories: ['database'], }, { obj: federationv2PackageManifest, + installState: 'Not Installed', kind: 'PackageManifest', name: 'federationv2', uid: 'federationv2/openshift-operator-lifecycle-manager', @@ -365,6 +368,7 @@ export const operatorHubTileViewPageProps = { categories: [], }, { obj: prometheusPackageManifest, + installState: 'Not Installed', kind: 'PackageManifest', name: 'prometheus', uid: 'prometheus/openshift-operator-lifecycle-manager', @@ -384,6 +388,7 @@ export const operatorHubTileViewPageProps = { categories: ['monitoring', 'alerting'], }, { obj: svcatPackageManifest, + installState: 'Not Installed', kind: 'PackageManifest', name: 'svcat', uid: 'svcat/openshift-operator-lifecycle-manager', @@ -440,6 +445,8 @@ export const operatorHubTileViewPagePropsWithDummy = { export const filterCounts = { CoreOS: 1, 'Red Hat': 4, + Installed: 1, + 'Not Installed': 4, }; export const operatorHubCategories = [ diff --git a/frontend/__tests__/components/operator-hub/operator-hub.spec.tsx b/frontend/__tests__/components/operator-hub/operator-hub.spec.tsx index 86fb7a50eaa..d5b1e65ab36 100644 --- a/frontend/__tests__/components/operator-hub/operator-hub.spec.tsx +++ b/frontend/__tests__/components/operator-hub/operator-hub.spec.tsx @@ -95,7 +95,7 @@ describe(OperatorHubTileView.displayName, () => { const filterItems = wrapper.find(FilterSidePanel.CategoryItem); expect(filterItems.exists()).toBe(true); - expect(filterItems.length).toBe(2); // Filter by Provider + expect(filterItems.length).toBe(4); // Filter by Provider and Install State filterItems.forEach((filter) => { expect(filter.props().count).toBe(filterCounts[_.split(filter.childAt(0).text(), '(')[0]]); }); @@ -107,14 +107,14 @@ describe(OperatorHubTileView.displayName, () => { const filterItemsChanged = wrapper.find(FilterSidePanel.CategoryItem); expect(filterItemsChanged.exists()).toBe(true); - expect(filterItemsChanged.length).toEqual(3); // Filter by Provider + expect(filterItemsChanged.length).toEqual(5); // Filter by Provider and Install State wrapper.setProps(operatorHubTileViewPageProps); wrapper.update(); const filterItemsFinal = wrapper.find(FilterSidePanel.CategoryItem); expect(filterItemsFinal.exists()).toBe(true); - expect(filterItemsFinal.length).toEqual(2); // Filter by Provider + expect(filterItemsFinal.length).toEqual(4); // Filter by Provider and Install State }); it('renders category tabs', () => { diff --git a/frontend/public/components/operator-hub/operator-hub-items.tsx b/frontend/public/components/operator-hub/operator-hub-items.tsx index 99dee60a61d..2f344f8eaf2 100644 --- a/frontend/public/components/operator-hub/operator-hub-items.tsx +++ b/frontend/public/components/operator-hub/operator-hub-items.tsx @@ -126,6 +126,20 @@ const sortFilterValues = (values, field) => { const determineAvailableFilters = (initialFilters, items, filterGroups) => { const filters = _.cloneDeep(initialFilters); + // Always show both install state filters + filters.installState = { + Installed: { + label: 'Installed', + value: 'Installed', + active: false, + }, + 'Not Installed': { + label: 'Not Installed', + value: 'Not Installed', + active: false, + }, + }; + _.each(filterGroups, field => { const values = []; _.each(items, item => { @@ -162,6 +176,7 @@ export const keywordCompare = (filterString, item) => { } return item.name.toLowerCase().includes(filterString) || + _.get(item, 'obj.metadata.name', '').toLowerCase().includes(filterString) || (item.description && item.description.toLowerCase().includes(filterString)) || (item.tags && item.tags.includes(filterString)); }; diff --git a/frontend/public/components/utils/tile-view-page.jsx b/frontend/public/components/utils/tile-view-page.jsx index 9672428cc59..3e4d883060e 100644 --- a/frontend/public/components/utils/tile-view-page.jsx +++ b/frontend/public/components/utils/tile-view-page.jsx @@ -444,6 +444,7 @@ export class TileViewPage extends React.Component { const activeValues = getActiveValuesFromURL(availableFilters, filterGroups); this.setState({...this.getUpdatedState(categories, activeValues.selectedCategoryId, activeValues.activeFilters) }); + this.filterByKeywordInput.focus({preventScroll: true}); } componentWillUnmount() { @@ -515,7 +516,7 @@ export class TileViewPage extends React.Component { this.updateMountedState(this.getUpdatedState(categories, selectedCategoryId, clearedFilters)); - this.filterByKeywordInput.focus(); + this.filterByKeywordInput.focus({preventScroll: true}); } selectCategory(categoryId) { @@ -616,7 +617,6 @@ export class TileViewPage extends React.Component { this.filterByKeywordInput = ref} - autoFocus={true} placeholder="Filter by keyword..." bsClass="form-control" value={activeFilters.keyword.value} diff --git a/frontend/public/const.js b/frontend/public/const.js index 719de856306..c5fc9fa0ba5 100644 --- a/frontend/public/const.js +++ b/frontend/public/const.js @@ -31,8 +31,7 @@ export const COMMUNITY_PROVIDERS_WARNING_LOCAL_STORAGE_KEY = `${STORAGE_PREFIX}/ export const KUBE_ADMIN_USERNAME = 'kube:admin'; export const OPERATOR_HUB_CSC_BASE = 'installed'; -// TODO: Update this once we have the support link -export const RH_OPERATOR_SUPPORT_POLICY_LINK = 'https://access.redhat.com/articles/1067'; +export const RH_OPERATOR_SUPPORT_POLICY_LINK = 'https://access.redhat.com/third-party-software-support'; // Package manifests for the Operator Hub use this label. export const OPERATOR_HUB_LABEL = 'openshift-marketplace'; From 07cf16addf9317583caa1a97f23911bc1a62ad07 Mon Sep 17 00:00:00 2001 From: Robb Hamilton Date: Wed, 30 Jan 2019 09:23:16 -0500 Subject: [PATCH 14/24] Fix bug where catalog overlay is still open when canceling create --- frontend/public/components/catalog/catalog-item-details.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/public/components/catalog/catalog-item-details.jsx b/frontend/public/components/catalog/catalog-item-details.jsx index 9fe09fbf522..62905ac7133 100644 --- a/frontend/public/components/catalog/catalog-item-details.jsx +++ b/frontend/public/components/catalog/catalog-item-details.jsx @@ -71,7 +71,7 @@ export class CatalogTileDetails extends React.Component {
- {this.props.item.createLabel} + {this.props.item.createLabel} {tileProvider && } {supportUrl && } {creationTimestamp && } />} From a8aa6866ca320dd9bdd0b1805c573212f35ac884 Mon Sep 17 00:00:00 2001 From: Joseph Caiani Date: Wed, 30 Jan 2019 09:30:25 -0500 Subject: [PATCH 15/24] move logging from the console to a file to simplify trouble-shooting test failures --- frontend/integration-tests/protractor.conf.ts | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/frontend/integration-tests/protractor.conf.ts b/frontend/integration-tests/protractor.conf.ts index 82b6b2c1ac9..75e18a2da2e 100644 --- a/frontend/integration-tests/protractor.conf.ts +++ b/frontend/integration-tests/protractor.conf.ts @@ -7,6 +7,8 @@ import * as _ from 'lodash'; import { TapReporter } from 'jasmine-reporters'; import * as ConsoleReporter from 'jasmine-console-reporter'; import * as failFast from 'protractor-fail-fast'; +import { createWriteStream} from 'fs'; +import { format } from 'util'; const tap = !!process.env.TAP; @@ -56,23 +58,15 @@ export const config: Config = { } }, onComplete: async() => { - console.log('BEGIN BROWSER LOGS'); + const consoleLogStream = createWriteStream('gui_test_screenshots/browser.log', { flags: 'a' }); + this.logToFile = function() { + consoleLogStream.write(`${format.apply(null, arguments)}\n`); + }; browserLogs.forEach(log => { const {level, message} = log; const messageStr = _.isArray(message) ? message.join(' ') : message; - switch (level.name) { - case 'DEBUG': - console.log(level, messageStr); - break; - case 'SEVERE': - console.warn(level, messageStr); - break; - case 'INFO': - default: - console.info(level, messageStr); - } + this.logToFile(`[${level.name}]`, messageStr); }); - console.log('END BROWSER LOGS'); // Use projects if OpenShift so non-admin users can run tests. We need the fully-qualified name // since we're using kubectl instead of oc. From d43e53ff4a39a07cc3c7d71120544d1d1860e4a6 Mon Sep 17 00:00:00 2001 From: Jeffrey Phillips Date: Wed, 30 Jan 2019 10:38:03 -0500 Subject: [PATCH 16/24] Prevent operator icons from stretching in operator subscription page Fixes https://bugzilla.redhat.com/show_bug.cgi?id=1669304 --- .../clusterserviceversion.spec.tsx | 4 ---- frontend/public/components/catalog/_catalog.scss | 4 ++-- .../public/components/operator-lifecycle-manager/index.tsx | 6 +++--- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/frontend/__tests__/components/operator-lifecycle-manager/clusterserviceversion.spec.tsx b/frontend/__tests__/components/operator-lifecycle-manager/clusterserviceversion.spec.tsx index bf504896ca1..16118367975 100644 --- a/frontend/__tests__/components/operator-lifecycle-manager/clusterserviceversion.spec.tsx +++ b/frontend/__tests__/components/operator-lifecycle-manager/clusterserviceversion.spec.tsx @@ -120,8 +120,6 @@ describe(ClusterServiceVersionLogo.displayName, () => { it('renders logo image from given base64 encoded image string', () => { const image: ReactWrapper> = wrapper.find('img'); - expect(image.props().height).toEqual('40'); - expect(image.props().width).toEqual('40'); expect(image.props().src).toEqual(`data:${testClusterServiceVersion.spec.icon.mediatype};base64,${testClusterServiceVersion.spec.icon.base64data}`); }); @@ -130,8 +128,6 @@ describe(ClusterServiceVersionLogo.displayName, () => { const fallbackImg: ReactWrapper> = wrapper.find('img'); expect(fallbackImg.props().src).toEqual(operatorLogo); - expect(fallbackImg.props().height).toEqual('40'); - expect(fallbackImg.props().width).toEqual('40'); }); it('renders ClusterServiceVersion name and provider from given spec', () => { diff --git a/frontend/public/components/catalog/_catalog.scss b/frontend/public/components/catalog/_catalog.scss index a50e9bbd930..1337b4f38d2 100644 --- a/frontend/public/components/catalog/_catalog.scss +++ b/frontend/public/components/catalog/_catalog.scss @@ -86,7 +86,7 @@ $catalog-item-icon-size-sm: 24px; max-width: $catalog-item-icon-size-sm; &[src$=".svg"] { - height: $catalog-item-icon-size-sm; + width: $catalog-item-icon-size-sm; } &--large { @@ -94,7 +94,7 @@ $catalog-item-icon-size-sm: 24px; max-width: $catalog-item-icon-size-lg; &[src$=".svg"] { - height: $catalog-item-icon-size-lg; + width: $catalog-item-icon-size-lg; } } } diff --git a/frontend/public/components/operator-lifecycle-manager/index.tsx b/frontend/public/components/operator-lifecycle-manager/index.tsx index e89d4838cbc..1d230320bfd 100644 --- a/frontend/public/components/operator-lifecycle-manager/index.tsx +++ b/frontend/public/components/operator-lifecycle-manager/index.tsx @@ -240,11 +240,11 @@ export const referenceForProvidedAPI = (desc: CRDDescription | APIServiceDefinit export const ClusterServiceVersionLogo: React.SFC = (props) => { const {icon, displayName, provider, version} = props; + const imgSrc = _.isEmpty(icon) ? operatorLogo : `data:${icon.mediatype};base64,${icon.base64data}`; return
-
{ _.isEmpty(icon) - ? - : } +
+

{displayName}

From f7af630cfee0ad3eb7c2f22e6d7b8d0c7e7d681a Mon Sep 17 00:00:00 2001 From: Joseph Caiani Date: Wed, 30 Jan 2019 11:09:27 -0500 Subject: [PATCH 17/24] remove logging function since its now called only once. --- frontend/integration-tests/protractor.conf.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/frontend/integration-tests/protractor.conf.ts b/frontend/integration-tests/protractor.conf.ts index 75e18a2da2e..bb0668a0edb 100644 --- a/frontend/integration-tests/protractor.conf.ts +++ b/frontend/integration-tests/protractor.conf.ts @@ -59,13 +59,10 @@ export const config: Config = { }, onComplete: async() => { const consoleLogStream = createWriteStream('gui_test_screenshots/browser.log', { flags: 'a' }); - this.logToFile = function() { - consoleLogStream.write(`${format.apply(null, arguments)}\n`); - }; browserLogs.forEach(log => { const {level, message} = log; const messageStr = _.isArray(message) ? message.join(' ') : message; - this.logToFile(`[${level.name}]`, messageStr); + consoleLogStream.write(`${format.apply(null, [`[${level.name}]`, messageStr])}\n`); }); // Use projects if OpenShift so non-admin users can run tests. We need the fully-qualified name From 415a38c8b9f3076fd18cb461822964732ce807c5 Mon Sep 17 00:00:00 2001 From: sg00dwin Date: Mon, 14 Jan 2019 15:49:47 -0500 Subject: [PATCH 18/24] Incorporate vertical scroll shadows to modals - modal-content class is now a child of modal-dialog - shared css for react-modal and patternfly-react modal - add modal optional height classes - pf overrides for catalog height and positioning to support scroll shadows --- .../public/components/catalog/_catalog.scss | 13 +--- .../catalog/catalog-item-details.jsx | 48 ++++++------ frontend/public/components/factory/modal.tsx | 11 ++- .../public/components/modals/_modals.scss | 75 +++++++++++++------ .../modals/add-secret-to-workload.tsx | 2 +- .../modals/cluster-update-modal.tsx | 2 +- .../modals/configure-count-modal.jsx | 2 +- .../modals/configure-ns-pull-secret-modal.jsx | 2 +- .../configure-operator-channel-modal.jsx | 2 +- .../modals/configure-operator-modal.jsx | 2 +- .../modals/configure-unschedulable-modal.jsx | 2 +- .../configure-update-strategy-modal.jsx | 2 +- .../components/modals/confirm-modal.jsx | 2 +- .../modals/create-namespace-modal.jsx | 2 +- .../public/components/modals/delete-modal.jsx | 2 +- .../modals/delete-namespace-modal.jsx | 2 +- .../modals/disable-application-modal.tsx | 2 +- .../public/components/modals/error-modal.jsx | 2 +- .../modals/installplan-approval-modal.tsx | 2 +- .../public/components/modals/labels-modal.jsx | 2 +- .../modals/subscription-channel-modal.tsx | 2 +- frontend/public/components/modals/tags.jsx | 2 +- .../components/modals/token-info-modal.jsx | 2 +- .../operator-hub-item-details.tsx | 50 +++++++------ .../spec/resource-requirements.tsx | 2 +- frontend/public/style.scss | 1 + frontend/public/style/_overrides.scss | 24 ++++-- .../public/style/mixin/_scroll-shadows.scss | 15 ++++ 28 files changed, 173 insertions(+), 104 deletions(-) create mode 100644 frontend/public/style/mixin/_scroll-shadows.scss diff --git a/frontend/public/components/catalog/_catalog.scss b/frontend/public/components/catalog/_catalog.scss index a50e9bbd930..10f62367e20 100644 --- a/frontend/public/components/catalog/_catalog.scss +++ b/frontend/public/components/catalog/_catalog.scss @@ -140,19 +140,10 @@ $catalog-item-icon-size-sm: 24px; // Enable scrolling on the modal &__overlay { - .modal-body { - flex: 1 1 auto; - overflow-y: auto; - -webkit-overflow-scrolling: touch; - - .hint-block-pf { + .modal-body .hint-block-pf { margin-bottom: 10px; - } - } - .modal-content { - display: flex; - flex-direction: column; } + .properties-side-panel-pf { flex: 0 0 auto; } diff --git a/frontend/public/components/catalog/catalog-item-details.jsx b/frontend/public/components/catalog/catalog-item-details.jsx index 62905ac7133..c9b29527147 100644 --- a/frontend/public/components/catalog/catalog-item-details.jsx +++ b/frontend/public/components/catalog/catalog-item-details.jsx @@ -69,28 +69,32 @@ export class CatalogTileDetails extends React.Component { iconImg={tileImgUrl} /> -
- - {this.props.item.createLabel} - {tileProvider && } - {supportUrl && } - {creationTimestamp && } />} - -
- {tileDescription &&

{tileDescription}

} - {longDescription &&

{longDescription}

} - {sampleRepo &&

Sample repository: {sampleRepoLink}

} - {documentationUrl && -

Documentation

-

{documentationUrlLink}

-
} - {!_.isEmpty(plans) && -

Service Plans

-
    - {planItems} -
-
} - {kind === 'ImageStream' && } +
+
+
+ + {this.props.item.createLabel} + {tileProvider && } + {supportUrl && } + {creationTimestamp && } />} + +
+ {tileDescription &&

{tileDescription}

} + {longDescription &&

{longDescription}

} + {sampleRepo &&

Sample repository: {sampleRepoLink}

} + {documentationUrl && +

Documentation

+

{documentationUrlLink}

+
} + {!_.isEmpty(plans) && +

Service Plans

+
    + {planItems} +
+
} + {kind === 'ImageStream' && } +
+
diff --git a/frontend/public/components/factory/modal.tsx b/frontend/public/components/factory/modal.tsx index 20fe55c95d1..c6fd28cc034 100644 --- a/frontend/public/components/factory/modal.tsx +++ b/frontend/public/components/factory/modal.tsx @@ -35,7 +35,7 @@ export const createModalLauncher: CreateModalLauncher = (Component) => (props) = isOpen={true} contentLabel="Modal" onRequestClose={closeModal} - className="modal-dialog modal-content" + className="modal-dialog" overlayClassName="co-overlay" shouldCloseOnOverlayClick={!props.blocking}> @@ -48,7 +48,14 @@ export const createModalLauncher: CreateModalLauncher = (Component) => (props) = export const ModalTitle: React.SFC = ({children, className = 'modal-header'}) =>

{children}

; -export const ModalBody: React.SFC = ({children, className= 'modal-body'}) =>
{children}
; +export const ModalBody: React.SFC = ({children, className= 'modal-body'}) => ( +
+
+
{children}
+
+
+); + export const ModalFooter: React.SFC = ({message, errorMessage, inProgress, children}) => { return diff --git a/frontend/public/components/modals/_modals.scss b/frontend/public/components/modals/_modals.scss index 525450d292f..20be2e9dde8 100644 --- a/frontend/public/components/modals/_modals.scss +++ b/frontend/public/components/modals/_modals.scss @@ -26,42 +26,75 @@ } } +// Modal modifications to enable vertical scrolling with shadow overlays .modal-body { display: flex; + flex: 1 1 auto; flex-direction: column; + height: 100%; overflow-y: auto; - -webkit-overflow-scrolling: touch; // enable momentum scrolling in mobile Safari + padding: 0; + @include scroll-shadows-vertical; + -webkit-overflow-scrolling: touch; +} + +.modal-body-content { + height: 100%; +} + +.modal-body-inner-shadow-covers { + min-height: 100%; + padding: ($grid-gutter-width / 2) $modal-title-padding-horizontal; + @include scroll-shadows-vertical-covers; + width: 100%; + + // so that input, textarea, button, and input-group-addon don't mask the inner scroll shadows + input, textarea { + &.form-control { + background-color: transparent; + &[disabled], + &[readonly] { + background-color: rgba(234, 234, 234, 0.5); + } + } + } - @media(max-width: $screen-xs-max) { - bottom: 75px; - height: auto; - left: 11px; - position: fixed; - right: 11px; - top: 54px; - width: auto; + .input-group-addon { + background-color: rgba(227, 227, 227, 0.5); } +} +.modal-content { + display: flex; + flex-direction: column; + height: 100%; @media(min-width: $screen-sm-min) { - max-height: calc(100vh - 165px); // 165px = 30 (margin-top) + 44 (modal-header) + 15 (modal-footer margin-top) + 44 (modal-footer) + 30 (margin-bottom) + 2px (top/bottom border) - min-height: 200px; + max-height: 415px; + } + min-height: 250px; + position: relative; +} + +@media(min-width: $screen-sm-min) { + .modal-content--large { + max-height: 530px; } + .modal-content--small { + max-height: 300px; + } } +// setting a height on modal-dialog enables flex child height to shrink and become scrollable .modal-dialog { - @media(max-width: $screen-xs-max) { - height: calc(100% - 20px); + height: calc(100% - 20px); // subtract height margin-top 10px + margin-bottom 10px + outline: 0; + + @media(min-width: $screen-sm-min) { + height: calc(100% - 60px); // subtract height margin-top 30px + margin-bottom 30px } } .modal-footer { - bottom: 0; - left: 0; - padding-top: 0; - position: absolute; - right: 0; - @media(min-width: $screen-sm-min) { - position: relative; - } + margin-top: 0; } diff --git a/frontend/public/components/modals/add-secret-to-workload.tsx b/frontend/public/components/modals/add-secret-to-workload.tsx index c102cabe8a2..834dab038fe 100644 --- a/frontend/public/components/modals/add-secret-to-workload.tsx +++ b/frontend/public/components/modals/add-secret-to-workload.tsx @@ -163,7 +163,7 @@ export class AddSecretToWorkloadModal extends React.Component + return Add Secret to Workload

diff --git a/frontend/public/components/modals/cluster-update-modal.tsx b/frontend/public/components/modals/cluster-update-modal.tsx index 738fdbd9f6c..1be36ab082a 100644 --- a/frontend/public/components/modals/cluster-update-modal.tsx +++ b/frontend/public/components/modals/cluster-update-modal.tsx @@ -41,7 +41,7 @@ class ClusterUpdateModal extends PromiseComponent { const currentVersion = getCurrentClusterVersion(cv); const dropdownItems = _.map(availableUpdates, 'version'); const dropdownTitle = _.get(availableUpdates[selectedVersion], 'version'); - return + return Update Cluster

diff --git a/frontend/public/components/modals/configure-count-modal.jsx b/frontend/public/components/modals/configure-count-modal.jsx index 9684d06eb57..edf81a52b78 100644 --- a/frontend/public/components/modals/configure-count-modal.jsx +++ b/frontend/public/components/modals/configure-count-modal.jsx @@ -55,7 +55,7 @@ class ConfigureCountModal extends PromiseComponent { } render() { - return + return {this.props.title}

{this.props.message}

diff --git a/frontend/public/components/modals/configure-ns-pull-secret-modal.jsx b/frontend/public/components/modals/configure-ns-pull-secret-modal.jsx index bd2374870a3..302ea8c8e56 100644 --- a/frontend/public/components/modals/configure-ns-pull-secret-modal.jsx +++ b/frontend/public/components/modals/configure-ns-pull-secret-modal.jsx @@ -169,7 +169,7 @@ class ConfigureNamespacePullSecret extends PromiseComponent { const existingData = parseExisitingPullSecret(pullSecret); - return + return Default Pull Secret

diff --git a/frontend/public/components/modals/configure-operator-channel-modal.jsx b/frontend/public/components/modals/configure-operator-channel-modal.jsx index 1d51a808aac..247d9a9912a 100644 --- a/frontend/public/components/modals/configure-operator-channel-modal.jsx +++ b/frontend/public/components/modals/configure-operator-channel-modal.jsx @@ -38,7 +38,7 @@ class ConfigureOperatorChannel extends PromiseComponent { 'tectonic-1.9-preproduction': 'Tectonic-1.9-preproduction', 'tectonic-1.9-production': 'Tectonic-1.9-production', }; - return + return Update Channel

diff --git a/frontend/public/components/modals/configure-operator-modal.jsx b/frontend/public/components/modals/configure-operator-modal.jsx index 3da487ef840..8f3b8cf608c 100644 --- a/frontend/public/components/modals/configure-operator-modal.jsx +++ b/frontend/public/components/modals/configure-operator-modal.jsx @@ -43,7 +43,7 @@ class ConfigureOperatorModal extends PromiseComponent { } render() { - return + return {this.props.title}
{this.props.message}
diff --git a/frontend/public/components/modals/configure-unschedulable-modal.jsx b/frontend/public/components/modals/configure-unschedulable-modal.jsx index 3b4be606ea3..3e79e23a5d4 100644 --- a/frontend/public/components/modals/configure-unschedulable-modal.jsx +++ b/frontend/public/components/modals/configure-unschedulable-modal.jsx @@ -23,7 +23,7 @@ class UnscheduleNodeModal extends PromiseComponent { } render() { - return + return Mark as Unschedulable Unschedulable nodes won't accept new pods. This is useful for scheduling maintenance or preparing to decommission a node. diff --git a/frontend/public/components/modals/configure-update-strategy-modal.jsx b/frontend/public/components/modals/configure-update-strategy-modal.jsx index 070071b55c9..301871e93ac 100644 --- a/frontend/public/components/modals/configure-update-strategy-modal.jsx +++ b/frontend/public/components/modals/configure-update-strategy-modal.jsx @@ -61,7 +61,7 @@ class ConfigureUpdateStrategyModal extends PromiseComponent { const maxUnavailable = _.get(this.deployment.spec, 'strategy.rollingUpdate.maxUnavailable', ''); const maxSurge = _.get(this.deployment.spec, 'strategy.rollingUpdate.maxSurge', ''); - return + return Edit Update Strategy
diff --git a/frontend/public/components/modals/confirm-modal.jsx b/frontend/public/components/modals/confirm-modal.jsx index 9f9fe4ce6ec..66967e56d1c 100644 --- a/frontend/public/components/modals/confirm-modal.jsx +++ b/frontend/public/components/modals/confirm-modal.jsx @@ -22,7 +22,7 @@ class ConfirmModal extends PromiseComponent { } render() { - return + return {this.props.title} {this.props.message} diff --git a/frontend/public/components/modals/create-namespace-modal.jsx b/frontend/public/components/modals/create-namespace-modal.jsx index 9d5a9a91a2f..11f9ce9d82e 100644 --- a/frontend/public/components/modals/create-namespace-modal.jsx +++ b/frontend/public/components/modals/create-namespace-modal.jsx @@ -101,7 +101,7 @@ const CreateNamespaceModal = connect(null, mapDispatchToProps)(class CreateNames [allow]: 'No restrictions (default)', [deny]: 'Deny all inbound traffic', }; - return + return Create {label}
diff --git a/frontend/public/components/modals/delete-modal.jsx b/frontend/public/components/modals/delete-modal.jsx index e4bf001f3ce..c7b0771d4c4 100644 --- a/frontend/public/components/modals/delete-modal.jsx +++ b/frontend/public/components/modals/delete-modal.jsx @@ -43,7 +43,7 @@ class DeleteModal extends PromiseComponent { render() { const {kind, resource} = this.props; - return + return Delete {kind.label}
diff --git a/frontend/public/components/modals/delete-namespace-modal.jsx b/frontend/public/components/modals/delete-namespace-modal.jsx index 09b480accaf..d42b1476568 100644 --- a/frontend/public/components/modals/delete-namespace-modal.jsx +++ b/frontend/public/components/modals/delete-namespace-modal.jsx @@ -28,7 +28,7 @@ class DeleteNamespaceModal extends PromiseComponent { } render() { - return + return Delete {this.props.kind.label}
diff --git a/frontend/public/components/modals/disable-application-modal.tsx b/frontend/public/components/modals/disable-application-modal.tsx index 817a5fad511..21f546d1b70 100644 --- a/frontend/public/components/modals/disable-application-modal.tsx +++ b/frontend/public/components/modals/disable-application-modal.tsx @@ -38,7 +38,7 @@ export class DisableApplicationModal extends PromiseComponent { render() { const {name} = this.props.subscription.spec; - return + return Remove Subscription
diff --git a/frontend/public/components/modals/error-modal.jsx b/frontend/public/components/modals/error-modal.jsx index 96fee0388c1..8e287666d4d 100644 --- a/frontend/public/components/modals/error-modal.jsx +++ b/frontend/public/components/modals/error-modal.jsx @@ -5,7 +5,7 @@ import {createModalLauncher, ModalTitle, ModalBody, ModalFooter} from '../factor export const errorModal = createModalLauncher( ({error, cancel}) => { return ( -
+
Error {error} diff --git a/frontend/public/components/modals/installplan-approval-modal.tsx b/frontend/public/components/modals/installplan-approval-modal.tsx index e9ab20ea2d2..91dbdec0134 100644 --- a/frontend/public/components/modals/installplan-approval-modal.tsx +++ b/frontend/public/components/modals/installplan-approval-modal.tsx @@ -34,7 +34,7 @@ export class InstallPlanApprovalModal extends PromiseComponent { } render() { - return + return Change Update Approval Strategy
diff --git a/frontend/public/components/modals/labels-modal.jsx b/frontend/public/components/modals/labels-modal.jsx index 98283e94de6..9c781672644 100644 --- a/frontend/public/components/modals/labels-modal.jsx +++ b/frontend/public/components/modals/labels-modal.jsx @@ -50,7 +50,7 @@ class BaseLabelsModal extends PromiseComponent { render() { const { kind, resource, description, message, labelClassName } = this.props; - return + return Edit {description || 'Labels'}
diff --git a/frontend/public/components/modals/subscription-channel-modal.tsx b/frontend/public/components/modals/subscription-channel-modal.tsx index 4f351a60ac5..7401a80aae1 100644 --- a/frontend/public/components/modals/subscription-channel-modal.tsx +++ b/frontend/public/components/modals/subscription-channel-modal.tsx @@ -28,7 +28,7 @@ export class SubscriptionChannelModal extends PromiseComponent { } render() { - return + return Change Subscription Update Channel
diff --git a/frontend/public/components/modals/tags.jsx b/frontend/public/components/modals/tags.jsx index 157c01bac9d..5438ae167ed 100644 --- a/frontend/public/components/modals/tags.jsx +++ b/frontend/public/components/modals/tags.jsx @@ -56,7 +56,7 @@ class TagsModal extends PromiseComponent { render() { const {tags} = this.state; - return + return {this.props.title} diff --git a/frontend/public/components/modals/token-info-modal.jsx b/frontend/public/components/modals/token-info-modal.jsx index 73956696ffd..50d95eccb02 100644 --- a/frontend/public/components/modals/token-info-modal.jsx +++ b/frontend/public/components/modals/token-info-modal.jsx @@ -34,7 +34,7 @@ class TokenInfoModal extends PromiseComponent { e.stopPropagation(); this.props.close(e); }; - return
+ return
Token Information
{this.state.tokenReview}
diff --git a/frontend/public/components/operator-hub/operator-hub-item-details.tsx b/frontend/public/components/operator-hub/operator-hub-item-details.tsx index e2bfc39ae75..7aed13c87d7 100644 --- a/frontend/public/components/operator-hub/operator-hub-item-details.tsx +++ b/frontend/public/components/operator-hub/operator-hub-item-details.tsx @@ -88,29 +88,33 @@ export const OperatorHubItemDetails: React.SFC = ({ /> -
- - - - - - - - - - -
- {getHintBlock()} - {longDescription - ? - : description} +
+
+
+ + + + + + + + + + +
+ {getHintBlock()} + {longDescription + ? + : description} +
+
diff --git a/frontend/public/components/operator-lifecycle-manager/descriptors/spec/resource-requirements.tsx b/frontend/public/components/operator-lifecycle-manager/descriptors/spec/resource-requirements.tsx index cac9810d97c..d897aa919f9 100644 --- a/frontend/public/components/operator-lifecycle-manager/descriptors/spec/resource-requirements.tsx +++ b/frontend/public/components/operator-lifecycle-manager/descriptors/spec/resource-requirements.tsx @@ -26,7 +26,7 @@ export class ResourceRequirementsModal extends PromiseComponent { } render() { - return this.submit(e)}> + return this.submit(e)} className="modal-content"> {this.props.title}
diff --git a/frontend/public/style.scss b/frontend/public/style.scss index 5c2bc7a259d..9047bfa8d55 100644 --- a/frontend/public/style.scss +++ b/frontend/public/style.scss @@ -18,6 +18,7 @@ // Mixins @import "style/mixin/prefix"; @import "style/mixin/break-word"; +@import "style/mixin/scroll-shadows"; /* CUSTOM STYLES */ diff --git a/frontend/public/style/_overrides.scss b/frontend/public/style/_overrides.scss index 7527080da94..e091b78f985 100644 --- a/frontend/public/style/_overrides.scss +++ b/frontend/public/style/_overrides.scss @@ -38,7 +38,12 @@ tags-input .tags { @include transition(border-color ease-in-out .15s, box-shadow ease-in-out .15s); } .modal-body tags-input .tags { - min-height: 200px; + background-color: rgba(255, 255, 255, 0.5); // enable scroll-shadows to be seen + margin-bottom: ($grid-gutter-width / 2); + min-height: 120px; + .tag-item { + background-color: rgba(235,235,235,.5); // enable scroll-shadows to be seen + } } tags-input .tags.focused, tags-input .tags:focus { @@ -218,10 +223,19 @@ tags-input .autocomplete .suggestion-item em { } } -.modal.right-side-modal-pf .modal-dialog { - margin-top: $pf-4-nav-bar-height; // since PatternFly 4's masthead is taller than PatternFly 3's - .modal-content { - height: calc(100vh - #{$pf-4-nav-bar-height}); + + +.modal.right-side-modal-pf { + top: 76px; // since PatternFly 4's masthead is taller than PatternFly 3's + + .modal-dialog { + height: 100%; // Entend panel to bottom + margin-top: 0; // parent is positioned: fixed so margin isn't needed for positioning + + .modal-content { + height: 100%; // Use % instead of vh so that scroll-shadows can be used + max-height: none; + } } } diff --git a/frontend/public/style/mixin/_scroll-shadows.scss b/frontend/public/style/mixin/_scroll-shadows.scss new file mode 100644 index 00000000000..a0e26ea891d --- /dev/null +++ b/frontend/public/style/mixin/_scroll-shadows.scss @@ -0,0 +1,15 @@ +@mixin scroll-shadows-vertical($shadow-width: 90%, $shadow-opacity: 0.25) { + background-attachment: scroll; + background-image: radial-gradient(ellipse at top, rgba(0, 0, 0, $shadow-opacity) 0%, rgba(0, 0, 0, 0) $shadow-width), radial-gradient(ellipse at bottom, rgba(0, 0, 0, $shadow-opacity) 0%, rgba(0, 0, 0, 0) $shadow-width); + background-position: 0 0, 0 100%; + background-repeat: no-repeat; + background-size: 100% 5px; +} + +@mixin scroll-shadows-vertical-covers($shadow-cover-bg-color: rgba(255,255,255,1), $shadow-cover-bg-color-transparent: rgba(255,255,255,0)) { + background-attachment: local; + background-image: linear-gradient($shadow-cover-bg-color 30%, $shadow-cover-bg-color-transparent), linear-gradient($shadow-cover-bg-color-transparent, $shadow-cover-bg-color 70%); + background-position: 0 0, 0 100%; + background-repeat: no-repeat; + background-size: 100% 12px; +} From e2d25554005255390e94cc9dc36bd697cd14065f Mon Sep 17 00:00:00 2001 From: Robb Hamilton Date: Thu, 31 Jan 2019 09:32:51 -0500 Subject: [PATCH 19/24] Fix bug where annotation remove icon is incorrect color --- frontend/public/components/utils/_name-value-editor.scss | 2 -- frontend/public/components/utils/name-value-editor.jsx | 8 ++++---- frontend/public/style/_overrides.scss | 4 ++++ 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/frontend/public/components/utils/_name-value-editor.scss b/frontend/public/components/utils/_name-value-editor.scss index 12b0dfde157..7faa89b76c5 100644 --- a/frontend/public/components/utils/_name-value-editor.scss +++ b/frontend/public/components/utils/_name-value-editor.scss @@ -55,7 +55,6 @@ $drag-row-margin: 8px; } .pairs-list__action-icon { - color: $color-pf-black-900; margin-left: $drag-row-margin; width: auto; } @@ -74,7 +73,6 @@ $drag-row-margin: 8px; } .pairs-list__span-btns { - color: $color-pf-black-900; margin-left: -$drag-row-margin; white-space: pre; } diff --git a/frontend/public/components/utils/name-value-editor.jsx b/frontend/public/components/utils/name-value-editor.jsx index 17fccee55f3..426b34317d2 100644 --- a/frontend/public/components/utils/name-value-editor.jsx +++ b/frontend/public/components/utils/name-value-editor.jsx @@ -317,7 +317,7 @@ const PairElement = DragSource(DRAGGABLE_TYPE.ENV_ROW, pairSource, collectSource
this.node = node}> {allowSorting && !readOnly &&
- {connectDragSource()}
@@ -338,7 +338,7 @@ const PairElement = DragSource(DRAGGABLE_TYPE.ENV_ROW, pairSource, collectSource { !readOnly &&
-
@@ -401,7 +401,7 @@ const EnvFromPairElement = DragSource(DRAGGABLE_TYPE.ENV_FROM_ROW, pairSource, c
this.node = node}> { !readOnly &&
- {connectDragSource()}
@@ -415,7 +415,7 @@ const EnvFromPairElement = DragSource(DRAGGABLE_TYPE.ENV_FROM_ROW, pairSource, c { readOnly ? null :
-
diff --git a/frontend/public/style/_overrides.scss b/frontend/public/style/_overrides.scss index e091b78f985..cb3a56e08aa 100644 --- a/frontend/public/style/_overrides.scss +++ b/frontend/public/style/_overrides.scss @@ -155,6 +155,10 @@ tags-input .autocomplete .suggestion-item em { margin-left: 3px; } +.btn-link--inherit-color { + color: inherit; +} + .breadcrumb { margin-bottom: 0; padding-bottom: 12px; From 143128e9489c813bd5ffbdb06825c7992ec0b1d9 Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Thu, 24 Jan 2019 23:08:59 -0500 Subject: [PATCH 20/24] add filter bar to CSV list view --- .../clusterserviceversion.spec.tsx | 11 +++-- frontend/public/components/factory/list.tsx | 6 +++ .../clusterserviceversion.tsx | 44 +++++++++++++------ .../operator-lifecycle-manager/index.tsx | 1 - 4 files changed, 42 insertions(+), 20 deletions(-) diff --git a/frontend/__tests__/components/operator-lifecycle-manager/clusterserviceversion.spec.tsx b/frontend/__tests__/components/operator-lifecycle-manager/clusterserviceversion.spec.tsx index 16118367975..2a4d80a5a53 100644 --- a/frontend/__tests__/components/operator-lifecycle-manager/clusterserviceversion.spec.tsx +++ b/frontend/__tests__/components/operator-lifecycle-manager/clusterserviceversion.spec.tsx @@ -8,7 +8,7 @@ import * as _ from 'lodash-es'; import { ClusterServiceVersionsDetailsPage, ClusterServiceVersionsDetailsPageProps, ClusterServiceVersionDetails, ClusterServiceVersionDetailsProps, ClusterServiceVersionsPage, ClusterServiceVersionsPageProps, ClusterServiceVersionList, ClusterServiceVersionHeader, ClusterServiceVersionRow, ClusterServiceVersionRowProps, CRDCard, CRDCardRow } from '../../../public/components/operator-lifecycle-manager/clusterserviceversion'; import { ClusterServiceVersionKind, ClusterServiceVersionLogo, ClusterServiceVersionLogoProps, referenceForProvidedAPI, CSVConditionReason } from '../../../public/components/operator-lifecycle-manager'; import { DetailsPage, ListPage, ListHeader, ColHead, List, ListInnerProps } from '../../../public/components/factory'; -import { testClusterServiceVersion, testModel } from '../../../__mocks__/k8sResourcesMocks'; +import { testClusterServiceVersion } from '../../../__mocks__/k8sResourcesMocks'; import { Timestamp, MsgBox, ResourceLink, ResourceKebab, ErrorBoundary, LoadingBox, ScrollToTopOnMount, SectionHeading } from '../../../public/components/utils'; import { referenceForModel } from '../../../public/module/k8s'; import { ClusterServiceVersionModel } from '../../../public/models'; @@ -177,7 +177,7 @@ describe(CRDCard.displayName, () => { const crd = testClusterServiceVersion.spec.customresourcedefinitions.owned[0]; it('renders a card with title, body, and footer', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find('.co-crd-card__title').exists()).toBe(true); expect(wrapper.find('.co-crd-card__body').exists()).toBe(true); @@ -185,14 +185,13 @@ describe(CRDCard.displayName, () => { }); it('renders a link to create a new instance', () => { - const kindObj = _.cloneDeep({...testModel, verbs: ['create']}); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find('.co-crd-card__footer').find(Link).props().to).toEqual(`/k8s/ns/${testClusterServiceVersion.metadata.namespace}/${ClusterServiceVersionModel.plural}/${testClusterServiceVersion.metadata.name}/${referenceForProvidedAPI(crd)}/new`); }); - it('does not render link to create new instance if "create" not included in verbs for the model', () => { - const wrapper = shallow(); + it('does not render link to create new instance if `props.canCreate` is false', () => { + const wrapper = shallow(); expect(wrapper.find('.co-crd-card__footer').find(Link).exists()).toBe(false); }); diff --git a/frontend/public/components/factory/list.tsx b/frontend/public/components/factory/list.tsx index 62dfb056955..877f060d926 100644 --- a/frontend/public/components/factory/list.tsx +++ b/frontend/public/components/factory/list.tsx @@ -105,6 +105,12 @@ const listFilters = { } return filters.selected.has(resource.kind); }, + 'clusterserviceversion-status': (filters, csv) => { + if (!filters || !filters.selected || !filters.selected.size) { + return true; + } + return filters.selected.has(_.get(csv.status, 'reason')) || !_.includes(filters.all, _.get(csv.status, 'reason')); + }, 'build-status': (phases, build) => { if (!phases || !phases.selected || !phases.selected.size) { diff --git a/frontend/public/components/operator-lifecycle-manager/clusterserviceversion.tsx b/frontend/public/components/operator-lifecycle-manager/clusterserviceversion.tsx index 5ed857268aa..bc65c123de1 100644 --- a/frontend/public/components/operator-lifecycle-manager/clusterserviceversion.tsx +++ b/frontend/public/components/operator-lifecycle-manager/clusterserviceversion.tsx @@ -9,12 +9,11 @@ import { Alert } from 'patternfly-react'; import { ProvidedAPIsPage, ProvidedAPIPage } from './clusterserviceversion-resource'; import { DetailsPage, ListHeader, ColHead, List, ListPage } from '../factory'; import { withFallback } from '../utils/error-boundary'; -import { referenceForModel, referenceFor, K8sKind } from '../../module/k8s'; +import { referenceForModel, referenceFor, GroupVersionKind, K8sKind } from '../../module/k8s'; import { ClusterServiceVersionModel } from '../../models'; import { FLAGS as featureFlags } from '../../features'; import { ResourceEventStream } from '../events'; import { Conditions } from '../conditions'; -import { connectToModel } from '../../kinds'; import { ClusterServiceVersionKind, ClusterServiceVersionLogo, @@ -105,6 +104,13 @@ export const ClusterServiceVersionsPage = connect(stateToProps)((props: ClusterS Installed Operators are represented by Cluster Service Versions within this namespace. For more information, see the . Or create an Operator and Cluster Service Version using the .

; + const rowFilters = [{ + type: 'clusterserviceversion-status', + selected: [CSVConditionReason.CSVReasonInstallSuccessful], + reducer: (csv: ClusterServiceVersionKind) => _.get(csv.status, 'reason'), + items: [CSVConditionReason.CSVReasonInstallSuccessful, CSVConditionReason.CSVReasonCopied].map(status => ({id: status, title: status})), + }]; + return ; }); @@ -122,8 +129,8 @@ export const MarkdownView = (props: {content: string, outerScroll: boolean}) => return import('./markdown-view').then(c => c.SyncMarkdownView)} {...props} />; }; -export const CRDCard = connectToModel((props: CRDCardProps) => { - const {csv, crd, kindObj} = props; +export const CRDCard: React.SFC = (props) => { + const {csv, crd, canCreate} = props; const createRoute = `/k8s/ns/${csv.metadata.namespace}/${ClusterServiceVersionModel.plural}/${csv.metadata.name}/${referenceForProvidedAPI(crd)}/new`; return
@@ -135,17 +142,27 @@ export const CRDCard = connectToModel((props: CRDCardProps) => {

{crd.description}

-
- { (kindObj.verbs || []).some(v => v === 'create') && + { canCreate &&
+ Create New - } -
+ +
}
; -}); +}; + +const crdCardRowStateToProps = ({k8s}, {crdDescs}) => { + const models: K8sKind[] = crdDescs.map(desc => k8s.getIn(['RESOURCES', 'models', referenceForProvidedAPI(desc)])); + return { + crdDescs: crdDescs.filter(desc => models.find(m => referenceForModel(m) === referenceForProvidedAPI(desc))), + createable: models.filter(m => (m.verbs || []).includes('create')).map(m => referenceForModel(m)), + }; +}; -export const CRDCardRow: React.SFC = (props) =>
- {props.crdDescs.map((desc, i) => )} -
; +export const CRDCardRow = connect(crdCardRowStateToProps)( + (props: CRDCardRowProps) =>
+ {props.crdDescs.map((desc, i) => )} +
+); export const ClusterServiceVersionDetails: React.SFC = (props) => { const {spec, metadata, status = {} as ClusterServiceVersionKind['status']} = props.obj; @@ -269,12 +286,13 @@ export type ClusterServiceVersionListProps = { export type CRDCardProps = { crd: CRDDescription | APIServiceDefinition; csv: ClusterServiceVersionKind; - kindObj: K8sKind; + canCreate: boolean; }; export type CRDCardRowProps = { crdDescs: (CRDDescription | APIServiceDefinition)[]; csv: ClusterServiceVersionKind; + createable: GroupVersionKind[]; }; export type CRDCardRowState = { diff --git a/frontend/public/components/operator-lifecycle-manager/index.tsx b/frontend/public/components/operator-lifecycle-manager/index.tsx index 1d230320bfd..03200ae23dc 100644 --- a/frontend/public/components/operator-lifecycle-manager/index.tsx +++ b/frontend/public/components/operator-lifecycle-manager/index.tsx @@ -48,7 +48,6 @@ export enum CSVConditionReason { export enum InstallPlanApproval { Automatic = 'Automatic', Manual = 'Manual', - UpdateOnly = 'Update-Only', } export enum SubscriptionState { From 871c4ea71bf551ed753449e574c9b462dc273960 Mon Sep 17 00:00:00 2001 From: Robb Hamilton Date: Thu, 31 Jan 2019 15:21:48 -0500 Subject: [PATCH 21/24] Fix bug where .co-m-horizontal-nav__menu__secondary does not scroll in Chrome 70 Windows --- frontend/public/components/_horizontal-nav.scss | 5 ----- 1 file changed, 5 deletions(-) diff --git a/frontend/public/components/_horizontal-nav.scss b/frontend/public/components/_horizontal-nav.scss index 95f3ea6aae2..b5ccc2133a5 100644 --- a/frontend/public/components/_horizontal-nav.scss +++ b/frontend/public/components/_horizontal-nav.scss @@ -25,13 +25,8 @@ $co-m-horizontal-nav__menu-item-link-padding-lr: 15px; overflow-y: hidden; padding: 0; -webkit-overflow-scrolling: touch; - - &::-webkit-scrollbar { - display: none; - } } - .co-m-horizontal-nav__menu-item { font-size: 18px; a, From df4b535ef2b0b181b6ec73f25f678a71fd24d457 Mon Sep 17 00:00:00 2001 From: Samuel Padgett Date: Thu, 31 Jan 2019 14:15:17 -0500 Subject: [PATCH 22/24] Fix show community operator dialog styles --- .../views/operator-hub.view.ts | 2 +- .../public/components/catalog/_catalog.scss | 13 ++- .../operator-hub-community-provider-modal.tsx | 92 +++++++++---------- .../operator-hub/operator-hub-items.tsx | 29 +++--- 4 files changed, 65 insertions(+), 71 deletions(-) diff --git a/frontend/integration-tests/views/operator-hub.view.ts b/frontend/integration-tests/views/operator-hub.view.ts index 067852d4c91..201da110296 100644 --- a/frontend/integration-tests/views/operator-hub.view.ts +++ b/frontend/integration-tests/views/operator-hub.view.ts @@ -17,5 +17,5 @@ export const operatorCommunityWarningIsLoaded = () => browser.wait(until.presenc .then(() => browser.sleep(500)); export const operatorCommunityWarningIsClosed = () => browser.wait(until.not(until.presenceOf(communityWarningModal)), 1000) .then(() => browser.sleep(500)); -export const closeCommunityWarningModal = () => communityWarningModal.$('.close').click(); +export const closeCommunityWarningModal = () => communityWarningModal.$('.btn-default').click(); export const acceptCommunityWarningModal = () => communityWarningModal.$('.btn-primary').click(); diff --git a/frontend/public/components/catalog/_catalog.scss b/frontend/public/components/catalog/_catalog.scss index c33d8ce5d3f..1123fa7b724 100644 --- a/frontend/public/components/catalog/_catalog.scss +++ b/frontend/public/components/catalog/_catalog.scss @@ -1,5 +1,6 @@ $catalog-item-icon-size-lg: 40px; $catalog-item-icon-size-sm: 24px; +$co-modal-ignore-warning-icon-width: 30px; // Until Patternfly-React-Extensions is updated: https://github.com/patternfly/patternfly-react/issues/1146 .catalog-tile-pf-title, .properties-side-panel-pf-property-value { @@ -201,6 +202,15 @@ $catalog-item-icon-size-sm: 24px; margin-bottom: 0; padding-top: 15px; } + &__content { + display: flex; + } + &__icon { + font-size: $co-modal-ignore-warning-icon-width; + margin-right: 15px; + // Avoid the dialog shifting when the icon loads. + min-width: $co-modal-ignore-warning-icon-width; + } &__link { display: block; margin: 10px 0; @@ -209,9 +219,6 @@ $catalog-item-icon-size-sm: 24px; margin-left: 5px; } } - .modal-footer { - margin-top: 0; - } } .properties-side-panel-pf-property-label { diff --git a/frontend/public/components/operator-hub/operator-hub-community-provider-modal.tsx b/frontend/public/components/operator-hub/operator-hub-community-provider-modal.tsx index e55936befa4..32d7039499d 100644 --- a/frontend/public/components/operator-hub/operator-hub-community-provider-modal.tsx +++ b/frontend/public/components/operator-hub/operator-hub-community-provider-modal.tsx @@ -1,12 +1,13 @@ /* eslint-disable no-undef */ import * as React from 'react'; import * as _ from 'lodash-es'; -import { Checkbox, Icon, MessageDialog } from 'patternfly-react'; +import { Checkbox, Icon } from 'patternfly-react'; import { RH_OPERATOR_SUPPORT_POLICY_LINK } from '../../const'; +import { createModalLauncher, ModalTitle, ModalBody, ModalSubmitFooter } from '../factory/modal'; import { ExternalLink } from '../utils'; -export class OperatorHubCommunityProviderModal extends React.Component { +export class OperatorHubCommunityProviderModal extends React.Component { constructor(props) { super(props); this.state = { @@ -14,64 +15,55 @@ export class OperatorHubCommunityProviderModal extends React.Component { this.setState({ ignoreWarnings: _.get(event, 'target.checked', false) }); }; + submit = (event) => { + event.preventDefault(); + this.props.showCommunityOperators(this.state.ignoreWarnings); + this.props.close(); + }; + render() { - const {show, close} = this.props; const { ignoreWarnings } = this.state; - - const messageText = ( -

- These are operators which have not been vetted or verified by Red Hat. Community Operators should be used with - caution because their stability is unknown. Red Hat provides no support for Community Operators. - {RH_OPERATOR_SUPPORT_POLICY_LINK && ( - - - - )} - Do you want to show Community Operators in the Operator Hub? -

- ); - - const ignoreWarningsContent = ( - - Do not show this warning again - - ); - - return close(false, false)} - primaryAction={() => close(true, ignoreWarnings)} - secondaryAction={() => close(false, false)} - primaryActionButtonContent="Show Community Operators" - secondaryActionButtonContent="Cancel" - title="Show Community Operators" - icon={} - primaryContent={messageText} - secondaryContent={ignoreWarningsContent} - accessibleName="communityProviderWarningDialog" - accessibleDescription="communityProviderWarningContent" - />; + const submitButtonContent = Show Community Operators; + return + Show Community Operators + +
+
+ +
+
+

+ These are operators which have not been vetted or verified by Red Hat. Community Operators should be used with + caution because their stability is unknown. Red Hat provides no support for Community Operators. + {RH_OPERATOR_SUPPORT_POLICY_LINK && ( + + + + )} + Do you want to show Community Operators in the Operator Hub? +

+ + Do not show this warning again + +
+
+
+ + ; } } -export type MarketplaceCommunityProviderModalProps = { - show: boolean; - close: (show: boolean, ignoreWarnings: boolean) => void; +export type OperatorHubCommunityProviderModalProps = { + showCommunityOperators: (ignoreWarnings: boolean) => void; + close: () => void; }; -export type MarketplaceCommunityProviderModalState = { +export type OperatorHubCommunityProviderModalState = { ignoreWarnings: boolean; }; + +export const communityOperatorWarningModal = createModalLauncher(OperatorHubCommunityProviderModal); diff --git a/frontend/public/components/operator-hub/operator-hub-items.tsx b/frontend/public/components/operator-hub/operator-hub-items.tsx index 2f344f8eaf2..d52126107b9 100644 --- a/frontend/public/components/operator-hub/operator-hub-items.tsx +++ b/frontend/public/components/operator-hub/operator-hub-items.tsx @@ -13,7 +13,7 @@ import { requireOperatorGroup } from '../operator-lifecycle-manager/operator-gro import { normalizeIconClass } from '../catalog/catalog-item-icon'; import { TileViewPage, updateURLParams, getFilterSearchParam, updateActiveFilters } from '../utils/tile-view-page'; import { OperatorHubItemDetails } from './operator-hub-item-details'; -import { OperatorHubCommunityProviderModal } from './operator-hub-community-provider-modal'; +import { communityOperatorWarningModal } from './operator-hub-community-provider-modal'; const pageDescription = ( @@ -262,11 +262,11 @@ export const OperatorHubTileView = requireOperatorGroup( if (!includeCommunityOperators) { const ignoreWarning = localStorage.getItem(COMMUNITY_PROVIDERS_WARNING_LOCAL_STORAGE_KEY); if (ignoreWarning === 'true') { - this.showCommunityOperators(true); + this.showCommunityOperators(); return; } - this.setState({ communityModalShown: true }); + communityOperatorWarningModal({ showCommunityOperators: this.showCommunityOperators }); return; } @@ -288,20 +288,16 @@ export const OperatorHubTileView = requireOperatorGroup( this.setState({items: stateItems, includeCommunityOperators: false}); }; - showCommunityOperators = (show: boolean, ignoreWarning: boolean = false) => { - if (show) { - const { items } = this.props; - const params = new URLSearchParams(window.location.search); - params.set('community-operators', 'true'); - setURLParams(params); + showCommunityOperators = (ignoreWarning: boolean = false) => { + const { items } = this.props; + const params = new URLSearchParams(window.location.search); + params.set('community-operators', 'true'); + setURLParams(params); - this.setState({items, includeCommunityOperators: true, communityModalShown: false}); + this.setState({items, includeCommunityOperators: true}); - if (ignoreWarning) { - localStorage.setItem(COMMUNITY_PROVIDERS_WARNING_LOCAL_STORAGE_KEY, 'true'); - } - } else { - this.setState({communityModalShown: false} ); + if (ignoreWarning) { + localStorage.setItem(COMMUNITY_PROVIDERS_WARNING_LOCAL_STORAGE_KEY, 'true'); } }; @@ -355,7 +351,7 @@ export const OperatorHubTileView = requireOperatorGroup( } render() { - const { items, detailsItem, communityModalShown } = this.state; + const { items, detailsItem } = this.state; return {detailsItem && } - ; } } From 33559605a645e33c942698186161bd2d491c2d56 Mon Sep 17 00:00:00 2001 From: Jon Jackson Date: Tue, 29 Jan 2019 15:54:18 -0500 Subject: [PATCH 23/24] Improve e2e test reliability --- frontend/integration-tests/protractor.conf.ts | 7 ++ .../integration-tests/tests/crud.scenario.ts | 84 ++++++++++--------- .../tests/overview/overview.scenario.ts | 11 ++- .../tests/secrets.scenario.ts | 4 +- .../views/create-role-binding.view.ts | 16 ++++ frontend/integration-tests/views/crud.view.ts | 12 +-- .../views/environment.view.ts | 6 +- .../integration-tests/views/namespace.view.ts | 6 +- .../integration-tests/views/search.view.ts | 4 +- .../integration-tests/views/secrets.view.ts | 6 +- .../integration-tests/views/sidenav.view.ts | 2 +- .../views/source-to-image.view.ts | 9 +- frontend/integration-tests/views/yaml.view.ts | 23 +++-- 13 files changed, 113 insertions(+), 77 deletions(-) create mode 100644 frontend/integration-tests/views/create-role-binding.view.ts diff --git a/frontend/integration-tests/protractor.conf.ts b/frontend/integration-tests/protractor.conf.ts index f493a381bf6..4ee31147bc5 100644 --- a/frontend/integration-tests/protractor.conf.ts +++ b/frontend/integration-tests/protractor.conf.ts @@ -158,3 +158,10 @@ export const waitForCount = (elementArrayFinder, expectedCount) => { return expectedCount >= actualCount; }; }; + +export const waitForNone = (elementArrayFinder) => { + return async() => { + const count = await elementArrayFinder.count(); + return count === 0; + }; +}; diff --git a/frontend/integration-tests/tests/crud.scenario.ts b/frontend/integration-tests/tests/crud.scenario.ts index c6adc48069c..b255a2a9a9c 100644 --- a/frontend/integration-tests/tests/crud.scenario.ts +++ b/frontend/integration-tests/tests/crud.scenario.ts @@ -1,6 +1,6 @@ /* eslint-disable no-undef, no-unused-vars, no-console */ -import { browser, $, $$, by, ExpectedConditions as until, Key } from 'protractor'; +import { browser, $, $$, by, ExpectedConditions as until, Key, element } from 'protractor'; import { safeLoad, safeDump } from 'js-yaml'; import * as _ from 'lodash'; import { execSync } from 'child_process'; @@ -10,6 +10,7 @@ import { appHost, testName, checkLogs, checkErrors } from '../protractor.conf'; import * as crudView from '../views/crud.view'; import * as yamlView from '../views/yaml.view'; import * as namespaceView from '../views/namespace.view'; +import * as createRoleBindingView from '../views/create-role-binding.view'; const K8S_CREATION_TIMEOUT = 15000; @@ -69,7 +70,7 @@ describe('Kubernetes resource CRUD operations', () => { testObjs.forEach(({kind, namespaced = true}, resource) => { describe(kind, () => { - + const name = `${testName}-${kind.toLowerCase()}`; it('displays a list view for the resource', async() => { await browser.get(`${appHost}${namespaced ? `/k8s/ns/${testName}` : '/k8s/cluster'}/${resource}?name=${testName}`); await crudView.isLoaded(); @@ -77,7 +78,7 @@ describe('Kubernetes resource CRUD operations', () => { if (namespaced) { it('has a working namespace dropdown on namespaced objects', async() => { - expect(namespaceView.namespaceSelector.isPresent()).toBe(true); + await browser.wait(until.presenceOf(namespaceView.namespaceSelector)); expect(namespaceView.selectedNamespace.getText()).toEqual(testName); }); } else { @@ -87,28 +88,30 @@ describe('Kubernetes resource CRUD operations', () => { } it('displays a YAML editor for creating a new resource instance', async() => { - const exists = await crudView.createItemButton.isPresent(); - if (exists) { + const createDropdownIsPresent = await crudView.createItemButton.isPresent(); + if (createDropdownIsPresent) { await crudView.createItemButton.click(); await crudView.createYAMLLink.click(); } else { await crudView.createYAMLButton.click(); } - const formToYamlExists = await crudView.createYAMLLink.isPresent(); - if (formToYamlExists) { - crudView.createYAMLLink.click(); + browser.wait(until.and(crudView.untilNoLoadersPresent, until.presenceOf(element(by.cssContainingText('h1', 'Create'))))); + + const yamlLinkIsPresent = await crudView.createYAMLLink.isPresent(); + if (yamlLinkIsPresent) { + await crudView.createYAMLLink.click(); } await yamlView.isLoaded(); const content = await yamlView.editorContent.getText(); - const newContent = _.defaultsDeep({}, {metadata: {name: testName, labels: {[testLabel]: testName}}}, safeLoad(content)); + const newContent = _.defaultsDeep({}, {metadata: {name, labels: {[testLabel]: testName}}}, safeLoad(content)); await yamlView.setContent(safeDump(newContent)); - expect(yamlView.editorContent.getText()).toContain(testName); + expect(yamlView.editorContent.getText()).toContain(name); }); it('creates a new resource instance', async() => { - leakedResources.add(JSON.stringify({name: testName, plural: resource, namespace: namespaced ? testName : undefined})); + leakedResources.add(JSON.stringify({name, plural: resource, namespace: namespaced ? testName : undefined})); await yamlView.saveButton.click(); expect(crudView.errorMessage.isPresent()).toBe(false); @@ -117,26 +120,25 @@ describe('Kubernetes resource CRUD operations', () => { it('displays detail view for new resource instance', async() => { await browser.wait(until.presenceOf(crudView.actionsDropdown)); - expect(browser.getCurrentUrl()).toContain(`/${testName}`); - expect(crudView.resourceTitle.getText()).toEqual(testName); + expect(browser.getCurrentUrl()).toContain(`/${name}`); + expect(crudView.resourceTitle.getText()).toEqual(name); }); it('search view displays created resource instance', async() => { await browser.get(`${appHost}/search/${namespaced ? `ns/${testName}` : 'all-namespaces'}?kind=${kind}&q=${testLabel}%3d${testName}`); await crudView.resourceRowsPresent(); - await crudView.filterForName(testName); - await crudView.rowForName(testName).element(by.linkText(testName)).click(); - await browser.wait(until.urlContains(`/${testName}`)); - - expect(crudView.resourceTitle.getText()).toEqual(testName); + await crudView.filterForName(name); + await crudView.rowForName(name).element(by.linkText(name)).click(); + await browser.wait(until.urlContains(`/${name}`)); + expect(crudView.resourceTitle.getText()).toEqual(name); }); it('edit the resource instance', async() => { if (kind !== 'ServiceAccount') { await browser.get(`${appHost}/search/${namespaced ? `ns/${testName}` : 'all-namespaces'}?kind=${kind}&q=${testLabel}%3d${testName}`); - await crudView.filterForName(testName); + await crudView.filterForName(name); await crudView.resourceRowsPresent(); - await crudView.editRow(kind)(testName); + await crudView.editRow(kind)(name); } }); @@ -145,18 +147,17 @@ describe('Kubernetes resource CRUD operations', () => { await crudView.resourceRowsPresent(); // Filter by resource name to make sure the resource is on the first page of results. // Otherwise the tests fail since we do virtual scrolling and the element isn't found. - await crudView.filterForName(testName); - await crudView.deleteRow(kind)(testName); - - leakedResources.delete(JSON.stringify({name: testName, plural: resource, namespace: namespaced ? testName : undefined})); + await crudView.filterForName(name); + await crudView.deleteRow(kind)(name); + leakedResources.delete(JSON.stringify({name, plural: resource, namespace: namespaced ? testName : undefined})); }); }); }); - describe('Bindings', () => { + describe('Role Bindings', () => { const bindingName = `${testName}-cluster-admin`; - - it('clicks on the `create bindings` button', async() => { + const roleName = 'cluster-admin'; + it('displays "Create Role Binding" page', async() => { await browser.get(`${appHost}/k8s/all-namespaces/rolebindings`); await crudView.isLoaded(); await crudView.createYAMLButton.click(); @@ -164,17 +165,25 @@ describe('Kubernetes resource CRUD operations', () => { }); it('creates a RoleBinding', async() => { - await $('#role-binding-name').sendKeys(bindingName); - await $('#ns-dropdown').click().then(() => browser.actions().sendKeys(testName, Key.ARROW_DOWN, Key.ENTER).perform()); - await $('#role-dropdown').click().then(() => browser.actions().sendKeys('cluster-admin', Key.ARROW_DOWN, Key.ENTER).perform()); - await $('#subject-name').sendKeys('subject-name'); - leakedResources.add(JSON.stringify({name: bindingName, plural: 'rolebindings', namespace: testName})); + await browser.wait(crudView.untilNoLoadersPresent); + + // Role Binding specific actions + await createRoleBindingView.inputName(bindingName); + await createRoleBindingView.selectNamespace(testName); + expect(createRoleBindingView.getSelectedNamespace()).toEqual(testName); + await createRoleBindingView.selectRole(roleName); + expect(createRoleBindingView.getSelectedRole()).toEqual(roleName); + await createRoleBindingView.inputSubject('subject-name'); + await crudView.saveChangesBtn.click(); expect(crudView.errorMessage.isPresent()).toBe(false); + await browser.wait(until.presenceOf(element(by.cssContainingText('h1.co-m-pane__heading', bindingName)))); + leakedResources.add(JSON.stringify({name: bindingName, plural: 'rolebindings', namespace: testName})); }); - it('search view displays created RoleBinding', async() => { + it('displays created RoleBinding in list view', async() => { await browser.get(`${appHost}/k8s/ns/${testName}/rolebindings`); + await crudView.isLoaded(); await crudView.resourceRowsPresent(); // Filter by resource name to make sure the resource is on the first page of results. // Otherwise the tests fail since we do virtual scrolling and the element isn't found. @@ -247,7 +256,6 @@ describe('Kubernetes resource CRUD operations', () => { it('displays `CustomResourceDefinitions` list view', async() => { await browser.get(`${appHost}/k8s/cluster/customresourcedefinitions`); await crudView.isLoaded(); - expect(crudView.resourceRows.count()).not.toEqual(0); }); @@ -257,7 +265,6 @@ describe('Kubernetes resource CRUD operations', () => { await yamlView.setContent(safeDump(crd)); await yamlView.saveButton.click(); await browser.wait(until.urlContains(name), K8S_CREATION_TIMEOUT); - expect(crudView.errorMessage.isPresent()).toBe(false); }); @@ -268,14 +275,12 @@ describe('Kubernetes resource CRUD operations', () => { await crudView.isLoaded(); await crudView.createYAMLButton.click(); await yamlView.isLoaded(); - expect(yamlView.editorContent.getText()).toContain(`kind: CRD${testName}`); }); xit('creates a new custom resource instance', async() => { leakedResources.add(JSON.stringify({name, plural: 'customresourcedefinitions'})); await yamlView.saveButton.click(); - expect(crudView.errorMessage.isPresent()).toBe(false); }); @@ -295,7 +300,6 @@ describe('Kubernetes resource CRUD operations', () => { beforeAll(async() => { await browser.get(`${appHost}/k8s/ns/${testName}/${plural}/new`); await yamlView.isLoaded(); - const content = await yamlView.editorContent.getText(); const newContent = _.defaultsDeep({}, {metadata: {name, namespace: testName}}, safeLoad(content)); await yamlView.setContent(safeDump(newContent)); @@ -306,9 +310,9 @@ describe('Kubernetes resource CRUD operations', () => { it('displays modal for editing resource instance labels', async() => { await browser.wait(until.presenceOf(crudView.actionsDropdown)); await crudView.actionsDropdown.click(); - await browser.wait(until.presenceOf(crudView.actionsDropdownMenu), 500); + await browser.wait(until.presenceOf(crudView.actionsDropdownMenu)); await crudView.actionsDropdownMenu.element(by.linkText('Edit Labels')).click(); - await browser.wait(until.presenceOf($('.tags input')), 500); + await browser.wait(until.presenceOf($('.tags input'))); await $('.tags input').sendKeys(labelValue, Key.ENTER); // This only works because there's only one label await browser.wait(until.textToBePresentInElement($('.tags .tag-item'), labelValue), 1000); diff --git a/frontend/integration-tests/tests/overview/overview.scenario.ts b/frontend/integration-tests/tests/overview/overview.scenario.ts index 9af72114f23..16562321cfe 100644 --- a/frontend/integration-tests/tests/overview/overview.scenario.ts +++ b/frontend/integration-tests/tests/overview/overview.scenario.ts @@ -26,26 +26,25 @@ describe('Visiting Overview page', () => { overviewResources.forEach((kindModel) => { describe(kindModel.labelPlural, () => { + const resourceName = `${testName}-${kindModel.kind.toLowerCase()}`; beforeAll(async()=>{ - await crudView.createNamespacedTestResource(kindModel); + await crudView.createNamespacedTestResource(kindModel, resourceName); }); it(`displays a ${kindModel.id} in the project overview list`, async() => { await browser.wait(until.presenceOf(overviewView.projectOverview)); await overviewView.itemsAreVisible(); - await expect(overviewView.getProjectOverviewListItem(kindModel, testName).isPresent()).toBeTruthy(); + await expect(overviewView.getProjectOverviewListItem(kindModel, resourceName).isPresent()).toBeTruthy(); }); it(`shows ${kindModel.id} details sidebar when item is clicked`, async() => { - const overviewListItem = overviewView.getProjectOverviewListItem(kindModel, testName); + const overviewListItem = overviewView.getProjectOverviewListItem(kindModel, resourceName); await expect(overviewView.detailsSidebar.isPresent()).toBeFalsy(); await browser.wait(until.elementToBeClickable(overviewListItem)); await overviewListItem.click(); await overviewView.sidebarIsLoaded(); await expect(overviewView.detailsSidebar.isDisplayed()).toBeTruthy(); - const title = await overviewView.detailsSidebarTitle.getText(); - await expect(title).toContain(kindModel.kind); - await expect(title).toContain(testName); + await expect(overviewView.detailsSidebarTitle.getText()).toContain(resourceName); }); }); }); diff --git a/frontend/integration-tests/tests/secrets.scenario.ts b/frontend/integration-tests/tests/secrets.scenario.ts index e22172eca83..1cc2fececdd 100644 --- a/frontend/integration-tests/tests/secrets.scenario.ts +++ b/frontend/integration-tests/tests/secrets.scenario.ts @@ -169,7 +169,7 @@ describe('Interacting with the create secret forms', () => { it('creates registry credentials image secret', async() => { await secretsView.createSecret(secretsView.createImageSecretLink, testName, credentialsImageSecretName, async() => { - await browser.wait(until.presenceOf(secretsView.addSecretEntryLink)); + await browser.wait(until.and(crudView.untilNoLoadersPresent, until.presenceOf(secretsView.addSecretEntryLink))); await secretsView.addSecretEntryLink.click(); await secretsView.imageSecretForm.each(async(el, index) => { await el.$('input[name=address]').sendKeys(address + index); @@ -256,7 +256,7 @@ describe('Interacting with the create secret forms', () => { it('creates Key/Value secret', async() => { await secretsView.createSecret(secretsView.createGenericSecretLink, testName, keyValueSecretName, async() => { - await browser.wait(until.presenceOf(secretsView.addSecretEntryLink)); + await browser.wait(until.and(crudView.untilNoLoadersPresent, until.presenceOf(secretsView.addSecretEntryLink))); await secretsView.addSecretEntryLink.click(); await browser.wait(waitForCount($$('.co-file-dropzone__textarea'), 2)); await secretsView.genericSecretForm.each(async(el, index) => { diff --git a/frontend/integration-tests/views/create-role-binding.view.ts b/frontend/integration-tests/views/create-role-binding.view.ts new file mode 100644 index 00000000000..38fad9baf10 --- /dev/null +++ b/frontend/integration-tests/views/create-role-binding.view.ts @@ -0,0 +1,16 @@ +import { $, browser, ExpectedConditions as until, element, by } from 'protractor'; + +const selectFromDropdown = async(dropdownButton, text) => { + await dropdownButton.click(); + await browser.wait(until.presenceOf($('.dropdown--text-filter'))); + await browser.actions().sendKeys(text).perform(); + await element(by.cssContainingText('li[role=option] a', text)).click(); + await browser.wait(until.not(until.visibilityOf($('.dropdown-menu')))); +}; + +export const selectNamespace = (namespace) => selectFromDropdown($('#ns-dropdown'), namespace); +export const selectRole = (role) => selectFromDropdown($('#role-dropdown'), role); +export const getSelectedNamespace = () => $('#ns-dropdown .co-resource-link__resource-name').getText(); +export const getSelectedRole = () => $('#role-dropdown .co-resource-link__resource-name').getText(); +export const inputName = (name) => $('#role-binding-name').sendKeys(name); +export const inputSubject = (subject) => $('#subject-name').sendKeys(subject); diff --git a/frontend/integration-tests/views/crud.view.ts b/frontend/integration-tests/views/crud.view.ts index 53f2a73b3b1..8d11e642d73 100644 --- a/frontend/integration-tests/views/crud.view.ts +++ b/frontend/integration-tests/views/crud.view.ts @@ -3,7 +3,7 @@ import { safeDump, safeLoad } from 'js-yaml'; import { $, $$, browser, by, ExpectedConditions as until } from 'protractor'; import * as yamlView from './yaml.view'; -import { appHost, testName } from '../protractor.conf'; +import { appHost, testName, waitForNone } from '../protractor.conf'; export const createYAMLButton = $('#yaml-create'); export const createItemButton = $('#item-create'); @@ -16,7 +16,9 @@ export const cancelBtn = $('#cancel'); /** * Returns a promise that resolves after the loading spinner is not present. */ -export const isLoaded = () => browser.wait(until.presenceOf($('.loading-box__loaded')), 10000).then(() => browser.sleep(1000)); +export const untilLoadingBoxLoaded = until.presenceOf($('.loading-box__loaded')); +export const untilNoLoadersPresent = waitForNone($$('.co-m-loader')); +export const isLoaded = () => browser.wait(until.and(untilNoLoadersPresent, untilLoadingBoxLoaded)).then(() => browser.sleep(1000)); export const resourceRowsPresent = () => browser.wait(until.presenceOf($('.co-m-resource-icon + a')), 10000); export const resourceRows = $$('.co-resource-list__item'); @@ -130,14 +132,14 @@ export const deleteResource = async(resource: string, kind: string, name: string // Navigates to create new resource page, creates an example resource of the specified kind, // then navigates back to the original url. -export const createNamespacedTestResource = async(kindModel) => { +export const createNamespacedTestResource = async(kindModel, name) => { const next = await browser.getCurrentUrl(); await browser.get(`${appHost}/k8s/ns/${testName}/${kindModel.plural}/new`); await yamlView.isLoaded(); const content = await yamlView.editorContent.getText(); - const newContent = _.defaultsDeep({}, {metadata: {name: testName, labels: {automatedTestName: testName}}}, safeLoad(content)); + const newContent = _.defaultsDeep({}, {metadata: {name, labels: {automatedTestName: testName}}}, safeLoad(content)); await yamlView.setContent(safeDump(newContent)); - await browser.wait(until.textToBePresentInElement(yamlView.editorContent, testName)); + await browser.wait(until.textToBePresentInElement(yamlView.editorContent, name)); await yamlView.saveButton.click(); await browser.wait(until.presenceOf($(`.co-m-${kindModel.kind}`))); await browser.get(next); diff --git a/frontend/integration-tests/views/environment.view.ts b/frontend/integration-tests/views/environment.view.ts index 6b6a6f9ff29..18e8d260635 100644 --- a/frontend/integration-tests/views/environment.view.ts +++ b/frontend/integration-tests/views/environment.view.ts @@ -1,12 +1,12 @@ -import { $, $$, browser, ExpectedConditions as until } from 'protractor'; +import { $, $$, browser, ExpectedConditions as until, by, element } from 'protractor'; const BROWSER_TIMEOUT = 15000; const inputs = $$('.form-control'); export const rowsKey = $('[placeholder="name"]'); export const rowsValue = $('[placeholder="value"]'); -export const deleteBtn = $('.pairs-list__delete-icon'); -export const saveBtn = $$('.btn-primary').filter(link => link.getText().then(text => text.startsWith('Save'))).first(); +export const deleteBtn = $$('.pairs-list__delete-icon').first(); +export const saveBtn = element(by.cssContainingText('.btn.btn-primary', 'Save')); export const isLoaded = () => browser.wait(until.presenceOf(inputs.first()), BROWSER_TIMEOUT); diff --git a/frontend/integration-tests/views/namespace.view.ts b/frontend/integration-tests/views/namespace.view.ts index bcd5133fb0d..94d13a59a4e 100644 --- a/frontend/integration-tests/views/namespace.view.ts +++ b/frontend/integration-tests/views/namespace.view.ts @@ -1,5 +1,5 @@ -import { $$ } from 'protractor'; - -export const namespaceSelector = $$('.co-namespace-selector'); +import { $, $$ } from 'protractor'; +export const namespaceBar = $('.co-namespace-bar'); +export const namespaceSelector = $('.co-namespace-selector'); export const selectedNamespace = $$('.co-namespace-selector .dropdown .btn-link .btn-link__title').first(); diff --git a/frontend/integration-tests/views/search.view.ts b/frontend/integration-tests/views/search.view.ts index b982ec04b42..2ce6ebb82e4 100644 --- a/frontend/integration-tests/views/search.view.ts +++ b/frontend/integration-tests/views/search.view.ts @@ -1,11 +1,11 @@ -import { $, $$, browser, by, element, ExpectedConditions as until } from 'protractor'; +import { $, $$, browser, ExpectedConditions as until } from 'protractor'; const BROWSER_TIMEOUT = 15000; export const dropdown = $('.co-type-selector .btn-dropdown'); export const dropdownLinks = $$('.dropdown-menu a'); export const labelFilter = $('.co-m-selector-input input'); -export const linkForType = type => element(by.partialLinkText(type)); +export const linkForType = type => $(`#${type}-link`); export const selectSearchType = async(objectType: string) => { const isPresent = await dropdownLinks.isPresent(); diff --git a/frontend/integration-tests/views/secrets.view.ts b/frontend/integration-tests/views/secrets.view.ts index 51ad1e425d5..5c22103ec38 100644 --- a/frontend/integration-tests/views/secrets.view.ts +++ b/frontend/integration-tests/views/secrets.view.ts @@ -26,7 +26,7 @@ export const createImageSecretLink = $('#image-link'); export const createGenericSecretLink = $('#generic-link'); export const addSecretEntryLink = $('.co-create-secret-form__link--add-entry'); -export const removeSecretEntryLink = $('.co-create-secret-form__link--remove-entry .btn-link'); +export const removeSecretEntryLink = $$('.co-create-secret-form__link--remove-entry .btn-link').first(); export const imageSecretForm = $$('.co-create-image-secret__form'); export const genericSecretForm = $$('.co-create-generic-secret__form'); @@ -69,10 +69,10 @@ export const checkSecret = async(ns: string, name: string, keyValuesToCheck: Obj export const editSecret = async(ns: string, name: string, updateForm: Function) => { await crudView.actionsDropdown.click(); - await browser.wait(until.presenceOf(crudView.actionsDropdownMenu), 500); + await browser.wait(until.presenceOf(crudView.actionsDropdownMenu)); await crudView.actionsDropdownMenu.element(by.linkText('Edit Secret')).click(); await browser.wait(until.urlContains(`/k8s/ns/${ns}/secrets/${name}/edit`)); - await browser.wait(until.presenceOf(secretNameInput)); + await browser.wait(until.and(crudView.untilNoLoadersPresent, until.presenceOf(secretNameInput))); await updateForm(); await saveButton.click(); expect(crudView.errorMessage.isPresent()).toBe(false); diff --git a/frontend/integration-tests/views/sidenav.view.ts b/frontend/integration-tests/views/sidenav.view.ts index e994852bb48..98576057ed9 100644 --- a/frontend/integration-tests/views/sidenav.view.ts +++ b/frontend/integration-tests/views/sidenav.view.ts @@ -2,7 +2,7 @@ import { $$, by, browser, element, ExpectedConditions as until } from 'protractor'; -export const navSectionFor = (name: string) => element(by.cssContainingText('.pf-c-nav__item', name)); +export const navSectionFor = (name: string) => element(by.cssContainingText('.pf-c-nav > .pf-c-nav__list > .pf-c-nav__item', name)); export const clickNavLink = (path: [string, string]) => browser.wait(until.visibilityOf(navSectionFor(path[0]))) .then(() => navSectionFor(path[0]).click()) diff --git a/frontend/integration-tests/views/source-to-image.view.ts b/frontend/integration-tests/views/source-to-image.view.ts index 78b237dd3dd..5c88e940e35 100644 --- a/frontend/integration-tests/views/source-to-image.view.ts +++ b/frontend/integration-tests/views/source-to-image.view.ts @@ -1,10 +1,10 @@ -import { browser, $, element, by, ExpectedConditions as until, Key } from 'protractor'; +import { browser, $, element, by, ExpectedConditions as until } from 'protractor'; import { appHost, testName } from '../protractor.conf'; import * as crudView from './crud.view'; export const createApplicationButton = element(by.buttonText('Create Application')); -export const isLoaded = () => browser.wait(until.presenceOf($('.co-source-to-image-form'))); +export const isLoaded = () => browser.wait(until.and(crudView.untilNoLoadersPresent, until.presenceOf($('.co-source-to-image-form')))); export const nsDropdown = $('#namespace'); export const nameInput = $('#name'); export const trySampleButton = element(by.partialButtonText('Try Sample')); @@ -17,5 +17,8 @@ export const visitOpenShiftImageStream = async(name: string) => { }; export const selectTestProject = async() => { - await nsDropdown.click().then(() => browser.actions().sendKeys(testName, Key.ARROW_DOWN, Key.ENTER).perform()); + await nsDropdown.click(); + await browser.wait(until.visibilityOf($('.dropdown-menu'))); + await element(by.cssContainingText('li[role=option] a', testName)).click(); + await browser.wait(until.not(until.visibilityOf($('.dropdown-menu')))); }; diff --git a/frontend/integration-tests/views/yaml.view.ts b/frontend/integration-tests/views/yaml.view.ts index b38b20b3175..d39051ced9f 100644 --- a/frontend/integration-tests/views/yaml.view.ts +++ b/frontend/integration-tests/views/yaml.view.ts @@ -1,16 +1,21 @@ /* eslint-disable no-undef, no-unused-vars */ - import { $, $$, by, Key, browser, ExpectedConditions as until } from 'protractor'; +import { waitForNone } from '../protractor.conf'; export const saveButton = $('.yaml-editor__buttons').$('#save-changes'); export const cancelButton = $('.yaml-editor__buttons').element(by.buttonText('Cancel')); - -export const isLoaded = () => browser.wait(until.visibilityOf(saveButton)); - -export const editorInput = $$('textarea.ace_text-input').get(0); +export const editorInput = $('textarea.ace_text-input'); export const editorContent = $('div.ace_content'); +export const isLoaded = () => browser.wait(until.and(waitForNone($$('.co-m-loader')), until.visibilityOf(saveButton))); +const insertLine = (line) => editorInput.sendKeys(line, Key.ENTER, Key.HOME); +export const setContent = async(text: string) => { + const lines = text.split(/\n/g); + await editorContent.click(); + await editorInput.sendKeys(Key.chord(Key.CONTROL, 'a'), Key.BACK_SPACE); + await editorInput.sendKeys(Key.chord(Key.COMMAND, 'a'), Key.BACK_SPACE); // For those OSX users... -export const setContent = (text: string) => editorContent.click() - .then(() => editorInput.sendKeys(Key.chord(Key.CONTROL, 'a'), Key.BACK_SPACE)) - .then(() => editorInput.sendKeys(Key.chord(Key.COMMAND, 'a'), Key.BACK_SPACE)) // For those OSX users... - .then(() => text.split(/\n/g).map(line => editorInput.sendKeys(line, Key.ENTER, Key.HOME))); + // Lines should be inserted sychronously and sequentially + for (const line of lines) { + await insertLine(line); + } +}; From ffdd06460060e4bce3eaa7804b9ff21049467da9 Mon Sep 17 00:00:00 2001 From: Samuel Padgett Date: Mon, 4 Feb 2019 15:54:20 -0500 Subject: [PATCH 24/24] Update OpenShift flag detection to use an endpoint other than `/oapi` The `/opai` endpoint has gone away. --- frontend/public/features.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/public/features.ts b/frontend/public/features.ts index 28e185227f4..24aed5b2227 100644 --- a/frontend/public/features.ts +++ b/frontend/public/features.ts @@ -93,8 +93,7 @@ const handleError = (res, flag, dispatch, cb) => { } }; -// FIXME: /oapi is deprecated. What else can we use to detect OpenShift? -const openshiftPath = `${k8sBasePath}/oapi/v1`; +const openshiftPath = `${k8sBasePath}/apis/apps.openshift.io/v1`; const detectOpenShift = dispatch => coFetchJSON(openshiftPath) .then( res => setFlag(dispatch, FLAGS.OPENSHIFT, _.size(res.resources) > 0),