From b50693abf125f286d64cd8c9cde8456740ab96b1 Mon Sep 17 00:00:00 2001 From: Brandon Walker Date: Wed, 5 Feb 2020 17:51:15 -0500 Subject: [PATCH] Support Triggers 0.2 features in the Dashboard Fixes #944 Updates the EventListener details page with the following: - Multiple Interceptors - Multiple TriggerBindings - No more params - Display all 4 built-in types of Interceptors (Webhook, GitHub, GitLab, and CEL) --- .../src/components/Trigger/Trigger.js | 313 ++++++++++++------ .../src/components/Trigger/Trigger.scss | 53 ++- .../src/components/Trigger/Trigger.stories.js | 145 ++++++++ .../src/components/Trigger/Trigger.test.js | 263 ++++++++++----- src/containers/EventListener/EventListener.js | 128 ++++--- .../EventListener/EventListener.scss | 18 + .../EventListener/EventListener.stories.js | 192 +++++++++++ .../EventListener/EventListener.test.js | 208 +++++++++--- .../TriggerBinding/TriggerBinding.js | 80 ++--- .../TriggerTemplate/TriggerTemplate.js | 80 ++--- src/nls/messages_en.json | 42 ++- src/scss/Triggers.scss | 6 + 12 files changed, 1137 insertions(+), 391 deletions(-) create mode 100644 packages/components/src/components/Trigger/Trigger.stories.js create mode 100644 src/containers/EventListener/EventListener.scss create mode 100644 src/containers/EventListener/EventListener.stories.js diff --git a/packages/components/src/components/Trigger/Trigger.js b/packages/components/src/components/Trigger/Trigger.js index e65a262d07..63ed0c614e 100644 --- a/packages/components/src/components/Trigger/Trigger.js +++ b/packages/components/src/components/Trigger/Trigger.js @@ -11,14 +11,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Launch16 as LinkIcon } from '@carbon/icons-react'; import { injectIntl } from 'react-intl'; import React from 'react'; import { urls } from '@tektoncd/dashboard-utils'; import { Link } from 'react-router-dom'; -import './Trigger.scss'; +import { Accordion, AccordionItem } from 'carbon-components-react'; import { Table } from '@tektoncd/dashboard-components'; +import './Trigger.scss'; + const Trigger = ({ intl, eventListenerNamespace, trigger }) => { const tableHeaders = [ { @@ -37,120 +38,228 @@ const Trigger = ({ intl, eventListenerNamespace, trigger }) => { } ]; - let triggerParams = []; - - if (trigger.params) { - triggerParams = trigger.params.map(param => ({ - id: param.name, - name: param.name, - value: param.value - })); - } - - let interceptorValues = []; - - if (trigger.interceptor) { - if (trigger.interceptor.header) { - interceptorValues = trigger.interceptor.header.map(header => ({ - id: header.name, - name: header.name, - value: header.value - })); - } - } - return ( <> -
-

Trigger: {trigger.name}

-
- TriggerBinding - - - - - - {trigger.binding.name} +

Trigger: {trigger.name}

+
+ {trigger.bindings && trigger.bindings.length !== 0 && ( +
+ + {intl.formatMessage({ + id: 'dashboard.triggerDetails.triggerBindings', + defaultMessage: 'TriggerBindings:' + })} - -
+
+ {trigger.bindings.map((binding, index) => ( + + + {binding.name} + + {index !== trigger.bindings.length - 1 && , } + + ))} +
+
+ )}
- TriggerTemplate + + {intl.formatMessage({ + id: 'dashboard.triggerDetails.triggerTemplate', + defaultMessage: 'TriggerTemplate:' + })} + - - - - - {trigger.template.name} - + {trigger.template.name}
+
-
-

- {intl.formatMessage({ - id: 'dashboard.parameters.title', - defaultMessage: 'Parameters' - })} -

- - - {trigger.interceptor && ( - <> -
-

- {intl.formatMessage({ - id: 'dashboard.triggerDetails.interceptorName', - defaultMessage: 'Interceptor: ' - })} - {trigger.interceptor.objectRef.name} -

-

- {intl.formatMessage({ - id: 'dashboard.triggerDetails.interceptorHeaders', - defaultMessage: 'Headers' - })} -

-
-
- - )} - +
+ {trigger.interceptors && trigger.interceptors.length !== 0 && ( + <> + + {intl.formatMessage({ + id: 'dashboard.triggerDetails.interceptors', + defaultMessage: 'Interceptors:' + })} + + + {trigger.interceptors.map((interceptor, index) => { + let interceptorName; + let interceptorType; + let content; + const namespaceText = intl.formatMessage({ + id: 'dashboard.triggerDetails.interceptorNamespace', + defaultMessage: 'Namespace:' + }); + const nameText = intl.formatMessage({ + id: 'dashboard.triggerDetails.interceptorName', + defaultMessage: 'Name:' + }); + if (interceptor.webhook) { + // Webhook Interceptor + if (!interceptor.webhook.objectRef) { + return null; + } + interceptorType = 'Webhook'; + interceptorName = interceptor.webhook.objectRef.name; + let headerValues = []; + if (interceptor.webhook.header) { + headerValues = interceptor.webhook.header.map(header => { + const headerValue = { + id: header.name, + name: header.name, + value: header.value + }; + // Concatenate values with a comma if value is an array + if (Array.isArray(header.value)) { + headerValue.value = header.value.join(', '); + } + return headerValue; + }); + } + const serviceText = intl.formatMessage({ + id: 'dashboard.triggerDetails.webhookInterceptorService', + defaultMessage: 'Service:' + }); + content = ( + <> +

{serviceText}

+
+

+ {nameText} {interceptor.webhook.objectRef.name} +

+ {interceptor.webhook.objectRef.namespace && ( +

+ {namespaceText}{' '} + {interceptor.webhook.objectRef.namespace} +

+ )} +
+ {headerValues.length !== 0 && ( + <> +

+ {intl.formatMessage({ + id: 'dashboard.triggerDetails.interceptorHeader', + defaultMessage: 'Header:' + })} +

+
+ + )} + + ); + } else if (interceptor.github || interceptor.gitlab) { + let data; + if (interceptor.github) { + // GitHub Interceptor + interceptorType = 'GitHub'; + data = interceptor.github; + } else { + // GitLab Interceptor + interceptorType = 'GitLab'; + data = interceptor.gitlab; + } + const eventTypes = data.eventTypes.join(', '); + interceptorName = eventTypes; + const secretText = intl.formatMessage({ + id: 'dashboard.triggerDetails.webhookInterceptorSecret', + defaultMessage: 'Secret:' + }); + const secretKeyText = intl.formatMessage({ + id: 'dashboard.triggerDetails.webhookInterceptorSecretKey', + defaultMessage: 'Key:' + }); + content = ( + <> +

{secretText}

+
+

+ {nameText} {data.secretRef.secretName} +

+

+ {secretKeyText} {data.secretRef.secretKey} +

+ {data.secretRef.namespace && ( +

+ {namespaceText} {data.secretRef.namespace} +

+ )} +
+

Event Types: {eventTypes}

+ + ); + } else if (interceptor.cel) { + // CEL Interceptor + interceptorType = 'CEL'; + interceptorName = interceptor.cel.filter; + const filter = intl.formatMessage({ + id: 'dashboard.triggerDetails.celInterceptorFilter', + defaultMessage: 'Filter: ' + }); + content = ( + <> +

{filter}

+ + {interceptor.cel.filter} + + + ); + } else { + return null; + } + const title = intl.formatMessage( + { + id: 'dashboard.triggerDetails.interceptorTitle', + defaultMessage: + '{interceptorNumber}. ({interceptorType}) {interceptorName}' + }, + { + interceptorNumber: index + 1, + interceptorType, + interceptorName + } + ); + return ( + + {content} + + ); + })} + + + )} ); diff --git a/packages/components/src/components/Trigger/Trigger.scss b/packages/components/src/components/Trigger/Trigger.scss index 7845f9800d..98395d508b 100644 --- a/packages/components/src/components/Trigger/Trigger.scss +++ b/packages/components/src/components/Trigger/Trigger.scss @@ -13,48 +13,43 @@ limitations under the License. @import '~carbon-components/scss/globals/scss/vars'; -.triggermain { - padding: 25px; - padding-left: 0px; - margin-bottom: 20px; - border-bottom: 0.5px dotted grey; -} +.trigger--interceptors { + margin-top: $spacing-05; -.interceptor h3 { - margin: 5px; - padding: 10px; + .trigger--interceptors-accordian { + margin-top: $spacing-03; + } } -.triggerinfo h3 { - margin: 5px; - padding: 10px; +.triggerdetails { + margin-top: $spacing-05; } .triggerresourcelinks { - margin: 10px; - padding: 10px; display: grid; - grid-template-columns: 100px 500px 100px; + margin-top: $spacing-03; + grid-template-columns: 100px 500px; grid-gap: 30px; - - .linkicon { - margin-right: 1rem; - } + line-height: 1.5rem; .resourcekind { - font-weight: bold; + font-weight: auto; } - - a { - display: flex; - overflow: hidden; - white-space: nowrap; + + .triggerresourcelink { + display: inline-block; + overflow: visible; } } -.truncate-text-end { +.interceptor--cel-filter { display: inline-block; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; +} + +.interceptor--secret-details { + margin-left: $spacing-04; +} + +.interceptor--service-details { + margin-left: $spacing-04; } diff --git a/packages/components/src/components/Trigger/Trigger.stories.js b/packages/components/src/components/Trigger/Trigger.stories.js new file mode 100644 index 0000000000..a8287a26e1 --- /dev/null +++ b/packages/components/src/components/Trigger/Trigger.stories.js @@ -0,0 +1,145 @@ +/* +Copyright 2019 The Tekton Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import { storiesOf } from '@storybook/react'; + +import { Router } from 'react-router-dom'; +import { IntlProvider } from 'react-intl'; +import { createMemoryHistory } from 'history'; +import Trigger from './Trigger'; + +const route = '/'; +const history = createMemoryHistory({ initialEntries: [route] }); +const props = { + eventListenerNamespace: 'default', + trigger: { + name: 'my-trigger', + bindings: [ + { name: 'triggerbinding0' }, + { name: 'triggerbinding1' }, + { name: 'triggerbinding2' } + ], + template: { + name: 'triggertemplate' + }, + interceptors: [ + { + webhook: { + header: [ + { + name: 'header0', + value: 'value0' + }, + { + name: 'header1', + value: ['value1-0', 'value1-1', 'value1-2'] + } + ], + objectRef: { + apiVersion: 'v1', + kind: 'Service', + name: 'interceptor-service0', + namespace: 'foo' + } + } + }, + { + github: { + secretRef: { + secretName: 'github-secret', + secretKey: 'secret' + }, + eventTypes: ['push', 'pull_request'] + } + }, + { + gitlab: { + secretRef: { + secretName: 'gitlab-secret', + secretKey: 'secret', + namespace: 'foo' + }, + eventTypes: ['Push Hook'] + } + }, + { + cel: { + filter: "body.matches('foo', 'bar')" + } + } + ] + } +}; + +storiesOf('Trigger', module) + .add('default', () => ( + + + + + + )) + .add('no name', () => ( + + + + + + )) + .add('no bindings', () => ( + + + + + + )) + .add('no interceptors', () => ( + + + + + + )) + .add('no headers in webhook interceptor', () => ( + + + + + + )); diff --git a/packages/components/src/components/Trigger/Trigger.test.js b/packages/components/src/components/Trigger/Trigger.test.js index fc7776130e..30bfe8bf07 100644 --- a/packages/components/src/components/Trigger/Trigger.test.js +++ b/packages/components/src/components/Trigger/Trigger.test.js @@ -16,131 +16,234 @@ import Trigger from './Trigger'; import { renderWithRouter } from '../../utils/test'; const fakeTrigger = { - binding: { - apiversion: 'v1alpha1', - name: 'simple-pipeline-push-binding' - }, - interceptor: { - header: [ - { - name: 'Wext-Trigger-Name', - value: 'mytrigger-tekton-pipelines-push-event' - }, - { - name: 'Wext-Repository-Url', - value: 'https://github.com/myorg/myrepo' - }, - { - name: 'Wext-Incoming-Event', - value: 'push' - }, - { - name: 'Wext-Secret-Name', - value: 'mytoken' - } - ], - objectRef: { - apiVersion: 'v1', - kind: 'Service', - name: 'tekton-webhooks-extension-validator', - namespace: 'tekton-pipelines' - } - }, - name: 'mytrigger-tekton-pipelines-push-event', - params: [ + name: 'my-fake-trigger', + bindings: [ { - name: 'webhooks-tekton-release-name', - value: 'myreleasename' + apiversion: 'v1alpha1', + name: 'triggerbinding-0' }, { - name: 'webhooks-tekton-target-namespace', - value: 'tekton-pipelines' + apiversion: 'v1alpha1', + name: 'triggerbinding-1' }, { - name: 'webhooks-tekton-service-account', - value: 'tekton-dashboard' - }, + apiversion: 'v1alpha1', + name: 'triggerbinding-2' + } + ], + template: { + apiversion: 'v1alpha1', + name: 'simple-pipeline-template' + }, + interceptors: [ { - name: 'webhooks-tekton-git-server', - value: 'github.com' + webhook: { + header: [ + { + name: 'Wext-Repository-Url', + value: 'https://github.com/myorg/myrepo' + }, + { + name: 'Wext-Incoming-Event', + value: 'wext-incoming-event-value' + }, + { + name: 'Wext-Secret-Name', + value: 'wext-secret-name-value' + }, + { + name: 'Array-Header-Name', + value: [ + 'array-header-value-0', + 'array-header-value-1', + 'array-header-value-2' + ] + } + ], + objectRef: { + apiVersion: 'v1', + kind: 'Service', + name: 'webhook-service-name', + namespace: 'webhook-service-namespace' + } + } }, { - name: 'webhooks-tekton-git-org', - value: 'myorg' + github: { + secretRef: { + secretName: 'my-github-secret', + secretKey: 'github-secret-key', + namespace: 'github-secret-namespace' + }, + eventTypes: ['github-event-0', 'github-event-1', 'github-event-2'] + } }, { - name: 'webhooks-tekton-git-repo', - value: 'myrepo' + gitlab: { + secretRef: { + secretName: 'my-gitlab-secret', + secretKey: 'gitlab-secret-key', + namespace: 'gitlab-secret-namespace' + }, + eventTypes: ['gitlab-event-0', 'gitlab-event-1', 'gitlab-event-2'] + } }, { - name: 'webhooks-tekton-docker-registry', - value: 'myregistry' + cel: { + filter: 'cel-filter' + } } - ], - template: { - apiversion: 'v1alpha1', - name: 'simple-pipeline-template' - } + ] }; describe('Trigger', () => { - it('should render all details', () => { + it('renders all details', () => { const props = { eventListenerNamespace: 'tekton-pipelines', trigger: fakeTrigger }; const { queryByText } = renderWithRouter(); expect(queryByText(/Name/i)).toBeTruthy(); + expect(queryByText(/my-fake-trigger/i)).toBeTruthy(); expect(queryByText(/TriggerBinding/i)).toBeTruthy(); + expect(queryByText(/triggerbinding-0/i)).toBeTruthy(); + expect(queryByText(/triggerbinding-1/i)).toBeTruthy(); + expect(queryByText(/triggerbinding-2/i)).toBeTruthy(); expect(queryByText(/TriggerTemplate/i)).toBeTruthy(); - expect(queryByText(/Interceptor/i)).toBeTruthy(); - expect(queryByText(/Headers/i)).toBeTruthy(); - expect(queryByText(/Parameters/i)).toBeTruthy(); - expect(queryByText(/webhooks-tekton-release-name/i)).toBeTruthy(); - expect(queryByText(/webhooks-tekton-target-namespace/i)).toBeTruthy(); - expect(queryByText(/webhooks-tekton-service-account/i)).toBeTruthy(); - expect(queryByText(/webhooks-tekton-git-server/i)).toBeTruthy(); - expect(queryByText(/webhooks-tekton-git-org/i)).toBeTruthy(); - expect(queryByText(/webhooks-tekton-git-repo/i)).toBeTruthy(); - expect(queryByText(/mytrigger-tekton-pipelines-push-event/i)).toBeTruthy(); - expect(queryByText(/myreleasename/i)).toBeTruthy(); - expect(queryByText(/tekton-pipelines/i)).toBeTruthy(); - expect(queryByText(/myregistry/i)).toBeTruthy(); - expect(queryByText(/myrepo/i)).toBeTruthy(); - expect(queryByText(/myorg/i)).toBeTruthy(); - expect(queryByText(/github.com/i)).toBeTruthy(); expect(queryByText(/simple-pipeline-template/i)).toBeTruthy(); - expect(queryByText(/Wext-Trigger-Name/i)).toBeTruthy(); + expect(queryByText(/Interceptors/i)).toBeTruthy(); + // Check Webhook Interceptor + expect(queryByText(/(webhook)/i)).toBeTruthy(); + expect(queryByText(/webhook-service-name/i)).toBeTruthy(); + expect(queryByText(/Header/i)).toBeTruthy(); + expect(queryByText(/webhook-service-namespace/i)).toBeTruthy(); expect(queryByText(/Wext-Repository-Url/i)).toBeTruthy(); + expect(queryByText(/https:\/\/github.com\/myorg\/myrepo/i)).toBeTruthy(); expect(queryByText(/Wext-Incoming-Event/i)).toBeTruthy(); + expect(queryByText(/wext-incoming-event-value/i)).toBeTruthy(); expect(queryByText(/Wext-Secret-Name/i)).toBeTruthy(); + expect(queryByText(/wext-secret-name-value/i)).toBeTruthy(); + expect(queryByText(/Array-Header-Name/i)).toBeTruthy(); + expect(queryByText(/array-header-value-0/i)).toBeTruthy(); + expect(queryByText(/array-header-value-1/i)).toBeTruthy(); + expect(queryByText(/array-header-value-2/i)).toBeTruthy(); + // Check GitHub Interceptor + expect(queryByText(/(github)/i)).toBeTruthy(); + expect(queryByText(/my-github-secret/i)).toBeTruthy(); + expect(queryByText(/github-secret-key/i)).toBeTruthy(); + expect(queryByText(/github-secret-namespace/i)).toBeTruthy(); + expect(queryByText(/github-event-0/i)).toBeTruthy(); + expect(queryByText(/github-event-1/i)).toBeTruthy(); + expect(queryByText(/github-event-2/i)).toBeTruthy(); + // Check GitLab Interceptor + expect(queryByText(/(gitlab)/i)).toBeTruthy(); + expect(queryByText(/my-gitlab-secret/i)).toBeTruthy(); + expect(queryByText(/gitlab-secret-key/i)).toBeTruthy(); + expect(queryByText(/gitlab-secret-namespace/i)).toBeTruthy(); + expect(queryByText(/gitlab-event-0/i)).toBeTruthy(); + expect(queryByText(/gitlab-event-1/i)).toBeTruthy(); + expect(queryByText(/gitlab-event-2/i)).toBeTruthy(); + // Check CEL Interceptor + expect(queryByText(/(cel)/i)).toBeTruthy(); + expect(queryByText(/cel-filter/i)).toBeTruthy(); }); - it('should handle missing params and interceptor', () => { + it('handles no objectRef in webhook Interceptor', () => { const props = { eventListenerNamespace: 'tekton-pipelines', trigger: { ...fakeTrigger, - params: undefined, - interceptor: undefined + interceptors: [ + { + webhook: {} + } + ] } }; const { queryByText } = renderWithRouter(); - expect(queryByText(/Name/i)).toBeTruthy(); + expect(queryByText(/1./i)).toBeFalsy(); }); - it('should handle missing interceptor headers', () => { + it('handles empty Interceptor', () => { const props = { eventListenerNamespace: 'tekton-pipelines', trigger: { ...fakeTrigger, - interceptor: { - ...fakeTrigger.interceptor, - header: undefined - } + interceptors: [{}] + } + }; + const { queryByText } = renderWithRouter(); + expect(queryByText(/Interceptors/i)).toBeTruthy(); + }); + + it('handles no Interceptors', () => { + const props = { + eventListenerNamespace: 'tekton-pipelines', + trigger: { + ...fakeTrigger, + interceptors: [] + } + }; + const { queryByText } = renderWithRouter(); + expect(queryByText(/Interceptors/i)).toBeFalsy(); + }); + + it('handles missing Interceptors', () => { + const props = { + eventListenerNamespace: 'tekton-pipelines', + trigger: { + ...fakeTrigger, + interceptors: undefined + } + }; + const { queryByText } = renderWithRouter(); + expect(queryByText(/Interceptors/i)).toBeFalsy(); + }); + + it('handles webhook Interceptor without Header', () => { + const props = { + eventListenerNamespace: 'tekton-pipelines', + trigger: { + ...fakeTrigger, + interceptors: [ + { + webhook: { + objectRef: { + apiVersion: 'v1', + kind: 'Service', + name: 'webhook-service-name', + namespace: 'webhook-service-namespace' + } + } + } + ] + } + }; + const { queryByText } = renderWithRouter(); + expect(queryByText(/Header/i)).toBeFalsy(); + }); + + it('handles no name', () => { + const props = { + eventListenerNamespace: 'tekton-pipelines', + trigger: { + ...fakeTrigger, + name: undefined + } + }; + const { queryByText } = renderWithRouter(); + expect(queryByText(/Trigger/i)).toBeTruthy(); + }); + + it('handles no bindings', () => { + const props = { + eventListenerNamespace: 'tekton-pipelines', + trigger: { + ...fakeTrigger, + bindings: undefined } }; const { queryByText } = renderWithRouter(); - expect(queryByText(/Interceptor/i)).toBeTruthy(); + expect(queryByText(/TriggerBindings/i)).toBeFalsy(); }); }); diff --git a/src/containers/EventListener/EventListener.js b/src/containers/EventListener/EventListener.js index 50aab07282..2fd7ac82ef 100644 --- a/src/containers/EventListener/EventListener.js +++ b/src/containers/EventListener/EventListener.js @@ -14,10 +14,8 @@ limitations under the License. import React, { Component } from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; -import '../../scss/Triggers.scss'; import { injectIntl } from 'react-intl'; import { InlineNotification, Tag } from 'carbon-components-react'; - import { formatLabels } from '@tektoncd/dashboard-utils'; import { FormattedDate, @@ -32,9 +30,11 @@ import { getSelectedNamespace, isWebSocketConnected } from '../../reducers'; - import { fetchEventListener } from '../../actions/eventListeners'; +import '../../scss/Triggers.scss'; +import './EventListener.scss'; + export /* istanbul ignore next */ class EventListenerContainer extends Component { static notification({ kind, message, intl }) { const titles = { @@ -129,54 +129,82 @@ export /* istanbul ignore next */ class EventListenerContainer extends Component })} >
-

- - {intl.formatMessage({ - id: 'dashboard.metadata.dateCreated', - defaultMessage: 'Date Created:' - })} - - -

-

- - {intl.formatMessage({ - id: 'dashboard.metadata.labels', - defaultMessage: 'Labels:' - })} - - {formattedLabelsToRender.length === 0 - ? intl.formatMessage({ - id: 'dashboard.metadata.none', - defaultMessage: 'None' - }) - : formattedLabelsToRender.map(label => ( - - {label} - - ))} -

-

- - {intl.formatMessage({ - id: 'dashboard.metadata.namespace', - defaultMessage: 'Namespace:' - })} - - {eventListener.metadata.namespace} -

- {triggers.map(trigger => { - return ( - +

+ + {intl.formatMessage({ + id: 'dashboard.metadata.dateCreated', + defaultMessage: 'Date Created:' + })}{' '} + + - ); - })} +

+

+ + {intl.formatMessage({ + id: 'dashboard.metadata.labels', + defaultMessage: 'Labels:' + })}{' '} + + {formattedLabelsToRender.length === 0 + ? intl.formatMessage({ + id: 'dashboard.metadata.none', + defaultMessage: 'None' + }) + : formattedLabelsToRender.map(label => ( + + {label} + + ))} +

+

+ + {intl.formatMessage({ + id: 'dashboard.metadata.namespace', + defaultMessage: 'Namespace:' + })}{' '} + + {eventListener.metadata.namespace} +

+ {eventListener.spec.serviceAccountName && ( +

+ + {intl.formatMessage({ + id: 'dashboard.metadata.serviceAccount', + defaultMessage: 'Service Account:' + })}{' '} + + {eventListener.spec.serviceAccountName} +

+ )} + {eventListener.spec.serviceType && ( +

+ + {intl.formatMessage({ + id: 'dashboard.metadata.serviceType', + defaultMessage: 'Service Type:' + })}{' '} + + {eventListener.spec.serviceType} +

+ )} +
+
+ {triggers.map((trigger, idx) => ( +
+ +
+ ))} +
diff --git a/src/containers/EventListener/EventListener.scss b/src/containers/EventListener/EventListener.scss new file mode 100644 index 0000000000..9475e399d5 --- /dev/null +++ b/src/containers/EventListener/EventListener.scss @@ -0,0 +1,18 @@ +/* +Copyright 2019 The Tekton Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +@import '~carbon-components/scss/globals/scss/vars'; + +.eventlistener--triggers { + margin-top: $layout-02; +} diff --git a/src/containers/EventListener/EventListener.stories.js b/src/containers/EventListener/EventListener.stories.js new file mode 100644 index 0000000000..631c5d268a --- /dev/null +++ b/src/containers/EventListener/EventListener.stories.js @@ -0,0 +1,192 @@ +/* +Copyright 2019 The Tekton Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import { storiesOf } from '@storybook/react'; + +import { Router } from 'react-router-dom'; +import { createIntl, IntlProvider } from 'react-intl'; +import { createMemoryHistory } from 'history'; +import { EventListenerContainer } from './EventListener'; + +const route = '/'; +const history = createMemoryHistory({ initialEntries: [route] }); +const eventListener = { + apiVersion: 'tekton.dev/v1alpha1', + kind: 'EventListener', + metadata: { + name: 'my-eventlistener', + namespace: 'foo', + creationTimestamp: '2020-01-31T16:39:21Z', + labels: { + foo: 'bar', + bar: 'baz' + } + }, + spec: { + serviceType: 'ClusterIP', + serviceAccountName: 'my-serviceaccount', + triggers: [ + { + eventListenerNamespace: 'default', + name: 'my-trigger', + bindings: [ + { name: 'triggerbinding0' }, + { name: 'triggerbinding1' }, + { name: 'triggerbinding2' } + ], + template: { + name: 'triggertemplate' + }, + interceptors: [ + { + webhook: { + header: [ + { + name: 'header0', + value: 'value0' + }, + { + name: 'header1', + value: ['value1-0', 'value1-1', 'value1-2'] + } + ], + objectRef: { + apiVersion: 'v1', + kind: 'Service', + name: 'interceptor-service0', + namespace: 'foo' + } + } + } + ] + }, + { + eventListenerNamespace: 'foo', + name: 'my-trigger-1', + bindings: [{ name: 'triggerbinding1' }], + template: { + name: 'triggertemplate1' + }, + interceptors: [ + { + gitlab: { + secretRef: { + secretName: 'gitlab-secret', + secretKey: 'secret', + namespace: 'foo' + }, + eventTypes: ['Push Hook'] + } + }, + { + cel: { + filter: "body.matches('foo', 'bar')" + } + } + ] + }, + { + eventListenerNamespace: 'foo', + name: 'my-trigger-2', + bindings: [{ name: 'triggerbinding2' }], + template: { + name: 'triggertemplate2' + }, + interceptors: [ + { + github: { + secretRef: { + secretName: 'github-secret', + secretKey: 'secret' + }, + eventTypes: ['push', 'pull_request'] + } + } + ] + }, + { + eventListenerNamespace: 'foo', + name: 'my-trigger-3', + bindings: [{ name: 'triggerbinding3' }], + template: { + name: 'triggertemplate3' + }, + interceptors: [] + } + ] + } +}; +const match = { + params: { + eventListenerName: 'my-eventlistener' + } +}; +const intl = createIntl({ + locale: 'en', + defaultLocale: 'en' +}); +const props = { + fetchEventListener: () => Promise.resolve(eventListener), + eventListener, + match, + intl +}; + +storiesOf('EventListenerContainer', module) + .add('default', () => ( + + + + + + )) + .add('no triggers', () => ( + + + + + + )) + .add('no serviceAccount', () => ( + + + + + + )) + .add('no service type', () => ( + + + + + + )); diff --git a/src/containers/EventListener/EventListener.test.js b/src/containers/EventListener/EventListener.test.js index 18fa9ccea6..799cc9d745 100644 --- a/src/containers/EventListener/EventListener.test.js +++ b/src/containers/EventListener/EventListener.test.js @@ -40,80 +40,196 @@ const fakeEventListenerWithLabels = { } }, spec: { + serviceType: 'NodePort', + serviceAccountName: 'my-serviceaccount', triggers: [ { - binding: { + name: 'my-trigger-0', + bindings: [ + { + apiversion: 'v1alpha1', + name: 'my-triggerbinding-0' + } + ], + template: { apiversion: 'v1alpha1', - name: 'simple-pipeline-push-binding' + name: 'my-triggertemplate-0' }, + interceptors: [ + { + cel: { filter: 'cel-filter-0' } + } + ] + }, + { + name: 'my-trigger-1', + bindings: [ + { + apiversion: 'v1alpha1', + name: 'my-triggerbinding-1' + } + ], template: { apiversion: 'v1alpha1', - name: 'simple-pipeline-template' + name: 'my-triggertemplate-1' }, - interceptor: { - header: [ - { - name: 'Wext-Trigger-Name', - value: 'mywebhook1-tekton-pipelines-push-event' - }, - { - name: 'Wext-Repository-Url', - value: 'https://github.com/myorg/myrepo' - }, - { - name: 'Wext-Incoming-Event', - value: 'push' - }, - { - name: 'Wext-Secret-Name', - value: 'mytoken' - } - ], - objectRef: { - apiVersion: 'v1', - kind: 'Service', - name: 'tekton-webhooks-extension-validator', - namespace: 'tekton-pipelines' + interceptors: [ + { + cel: { filter: 'cel-filter-1' } + } + ] + }, + { + name: 'my-trigger-2', + bindings: [ + { + apiversion: 'v1alpha1', + name: 'my-triggerbinding-2' } - } + ], + template: { + apiversion: 'v1alpha1', + name: 'my-triggertemplate-2' + }, + interceptors: [ + { + cel: { filter: 'cel-filter-2' } + } + ] } ] } }; +const match = { + params: { + eventListenerName: 'event-listener-with-labels' + } +}; + +const middleware = [thunk]; +const mockStore = configureStore(middleware); + +const testStore = mockStore({ + namespaces: { + selected: 'tekton-pipelines' + }, + eventListeners: { + byId: 'event-listener-with-labels', + byNamespace: { default: 'tekton-pipelines' }, + errorMessage: null, + isFetching: false + } +}); + it('EventListener displays with formatted labels', async () => { - const match = { - params: { - eventListenerName: 'event-listener-with-labels' + const { queryByText, getByText } = renderWithRouter( + + Promise.resolve(fakeEventListenerWithLabels)} + eventListener={fakeEventListenerWithLabels} + /> + + ); + + await waitForElement(() => getByText('event-listener-with-labels')); + expect(queryByText(/Namespace/i)).toBeTruthy(); + expect(queryByText(/tekton-pipelines/i)).toBeTruthy(); + expect(queryByText(/Labels/i)).toBeTruthy(); + expect(queryByText(/foo: bar/i)).toBeTruthy(); + expect(queryByText(/bar: baz/i)).toBeTruthy(); + expect(queryByText(/Service Account/i)).toBeTruthy(); + expect(queryByText(/my-serviceaccount/i)).toBeTruthy(); + expect(queryByText(/Service Type/i)).toBeTruthy(); + expect(queryByText(/NodePort/i)).toBeTruthy(); + // Check Trigger 0 + expect(queryByText(/my-trigger-0/i)).toBeTruthy(); + expect(queryByText(/my-triggerbinding-0/i)).toBeTruthy(); + expect(queryByText(/my-triggertemplate-0/i)).toBeTruthy(); + expect(queryByText(/cel-filter-0/i)).toBeTruthy(); + // Check Trigger 1 + expect(queryByText(/my-trigger-1/i)).toBeTruthy(); + expect(queryByText(/my-triggerbinding-1/i)).toBeTruthy(); + expect(queryByText(/my-triggertemplate-1/i)).toBeTruthy(); + expect(queryByText(/cel-filter-1/i)).toBeTruthy(); + // Check Trigger 2 + expect(queryByText(/my-trigger-2/i)).toBeTruthy(); + expect(queryByText(/my-triggerbinding-2/i)).toBeTruthy(); + expect(queryByText(/my-triggertemplate-2/i)).toBeTruthy(); + expect(queryByText(/cel-filter-2/i)).toBeTruthy(); +}); + +it('EventListener handles no serviceAccountName', async () => { + const eventListener = { + ...fakeEventListenerWithLabels, + spec: { + ...fakeEventListenerWithLabels.spec, + serviceAccountName: undefined } }; + const { queryByText, getByText } = renderWithRouter( + + Promise.resolve(eventListener)} + eventListener={eventListener} + /> + + ); - const middleware = [thunk]; - const mockStore = configureStore(middleware); + await waitForElement(() => getByText('event-listener-with-labels')); + expect(queryByText(/Service Account/i)).toBeFalsy(); +}); - const testStore = mockStore({ - namespaces: { - selected: 'tekton-pipelines' - }, - eventListeners: { - byId: 'event-listener-with-labels', - byNamespace: { default: 'tekton-pipelines' }, - errorMessage: null, - isFetching: false +it('EventListener handles no service type', async () => { + const eventListener = { + ...fakeEventListenerWithLabels, + spec: { + ...fakeEventListenerWithLabels.spec, + serviceType: undefined } - }); + }; + const { queryByText, getByText } = renderWithRouter( + + Promise.resolve(eventListener)} + eventListener={eventListener} + /> + + ); - const { getByText } = renderWithRouter( + await waitForElement(() => getByText('event-listener-with-labels')); + expect(queryByText(/Service Type/i)).toBeFalsy(); +}); + +it('EventListener handles no triggers', async () => { + const eventListener = { + ...fakeEventListenerWithLabels, + spec: { + ...fakeEventListenerWithLabels.spec, + triggers: [] + } + }; + const { queryByText, getByText } = renderWithRouter( Promise.resolve(fakeEventListenerWithLabels)} - eventListener={fakeEventListenerWithLabels} + fetchEventListener={() => Promise.resolve(eventListener)} + eventListener={eventListener} /> ); await waitForElement(() => getByText('event-listener-with-labels')); + expect(queryByText(/Trigger/i)).toBeFalsy(); }); diff --git a/src/containers/TriggerBinding/TriggerBinding.js b/src/containers/TriggerBinding/TriggerBinding.js index 0a3a2304ae..bf67f85cb9 100644 --- a/src/containers/TriggerBinding/TriggerBinding.js +++ b/src/containers/TriggerBinding/TriggerBinding.js @@ -157,45 +157,47 @@ export /* istanbul ignore next */ class TriggerBindingContainer extends Componen })} >
-

- - {intl.formatMessage({ - id: 'dashboard.metadata.dateCreated', - defaultMessage: 'Date Created:' - })} - - -

-

- - {intl.formatMessage({ - id: 'dashboard.metadata.labels', - defaultMessage: 'Labels:' - })} - - {formattedLabelsToRender.length === 0 - ? intl.formatMessage({ - id: 'dashboard.metadata.none', - defaultMessage: 'None' - }) - : formattedLabelsToRender.map(label => ( - - {label} - - ))} -

-

- - {intl.formatMessage({ - id: 'dashboard.metadata.namespace', - defaultMessage: 'Namespace:' - })} - - {triggerBinding.metadata.namespace} -

+
+

+ + {intl.formatMessage({ + id: 'dashboard.metadata.dateCreated', + defaultMessage: 'Date Created:' + })} + + +

+

+ + {intl.formatMessage({ + id: 'dashboard.metadata.labels', + defaultMessage: 'Labels:' + })} + + {formattedLabelsToRender.length === 0 + ? intl.formatMessage({ + id: 'dashboard.metadata.none', + defaultMessage: 'None' + }) + : formattedLabelsToRender.map(label => ( + + {label} + + ))} +

+

+ + {intl.formatMessage({ + id: 'dashboard.metadata.namespace', + defaultMessage: 'Namespace:' + })} + + {triggerBinding.metadata.namespace} +

+
-

- - {intl.formatMessage({ - id: 'dashboard.metadata.dateCreated', - defaultMessage: 'Date Created:' - })} - - -

-

- - {intl.formatMessage({ - id: 'dashboard.metadata.labels', - defaultMessage: 'Labels:' - })} - - {formattedLabelsToRender.length === 0 - ? intl.formatMessage({ - id: 'dashboard.metadata.none', - defaultMessage: 'None' - }) - : formattedLabelsToRender.map(label => ( - - {label} - - ))} -

-

- - {intl.formatMessage({ - id: 'dashboard.metadata.namespace', - defaultMessage: 'Namespace:' - })} - - {triggerTemplate.metadata.namespace} -

+
+

+ + {intl.formatMessage({ + id: 'dashboard.metadata.dateCreated', + defaultMessage: 'Date Created:' + })} + + +

+

+ + {intl.formatMessage({ + id: 'dashboard.metadata.labels', + defaultMessage: 'Labels:' + })} + + {formattedLabelsToRender.length === 0 + ? intl.formatMessage({ + id: 'dashboard.metadata.none', + defaultMessage: 'None' + }) + : formattedLabelsToRender.map(label => ( + + {label} + + ))} +

+

+ + {intl.formatMessage({ + id: 'dashboard.metadata.namespace', + defaultMessage: 'Namespace:' + })} + + {triggerTemplate.metadata.namespace} +

+