From d98b92da64e2d419d2199b8f0c81bd366e050958 Mon Sep 17 00:00:00 2001 From: Alan Greene <2829095+AlanGreene@users.noreply.github.com> Date: Wed, 17 Apr 2019 13:53:29 +0100 Subject: [PATCH 1/3] Add base redux config (#52) --- package-lock.json | 40 +++++++++++++++++++++++++++ package.json | 5 ++++ src/actions/pipeline.js | 29 +++++++++++++++++++ src/actions/pipeline.test.js | 40 +++++++++++++++++++++++++++ src/containers/App/App.test.js | 10 ++++++- src/containers/Pipelines/Pipelines.js | 9 ++++-- src/index.js | 9 +++++- src/reducers/index.js | 14 ++++++++++ src/reducers/pipeline.js | 23 +++++++++++++++ src/reducers/pipeline.test.js | 34 +++++++++++++++++++++++ src/store/index.js | 35 +++++++++++++++++++++++ 11 files changed, 244 insertions(+), 4 deletions(-) create mode 100644 src/actions/pipeline.js create mode 100644 src/actions/pipeline.test.js create mode 100644 src/reducers/index.js create mode 100644 src/reducers/pipeline.js create mode 100644 src/reducers/pipeline.test.js create mode 100644 src/store/index.js diff --git a/package-lock.json b/package-lock.json index 7059e8bf8..1524c3377 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9307,6 +9307,11 @@ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" }, + "lodash.keyby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.keyby/-/lodash.keyby-4.6.0.tgz", + "integrity": "sha1-f2oavak/0k4icopNNh7YvLpaQ1Q=" + }, "lodash.mergewith": { "version": "4.6.1", "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz", @@ -11698,6 +11703,19 @@ "react-popper": "^1.3.3" } }, + "react-redux": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.0.1.tgz", + "integrity": "sha512-orSiI/QXtGiiJmf8lN/zVTx4hysFo/kGOsce28IUu/mu98AGemBwPTDzf64P4Vf/miRmevO8/w2RSw2awDd21w==", + "requires": { + "@babel/runtime": "^7.4.3", + "hoist-non-react-statics": "^3.3.0", + "invariant": "^2.2.4", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^16.8.6" + } + }, "react-resize-detector": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/react-resize-detector/-/react-resize-detector-3.4.0.tgz", @@ -11952,6 +11970,28 @@ } } }, + "redux": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.1.tgz", + "integrity": "sha512-R7bAtSkk7nY6O/OYMVR9RiBI+XghjF9rlbl5806HJbQph0LJVHZrU5oaO4q70eUKiqMRqm4y07KLTlMZ2BlVmg==", + "requires": { + "loose-envify": "^1.4.0", + "symbol-observable": "^1.2.0" + } + }, + "redux-mock-store": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/redux-mock-store/-/redux-mock-store-1.5.3.tgz", + "integrity": "sha512-ryhkkb/4D4CUGpAV2ln1GOY/uh51aczjcRz9k2L2bPx/Xja3c5pSGJJPyR25GNVRXtKIExScdAgFdiXp68GmJA==", + "requires": { + "lodash.isplainobject": "^4.0.6" + } + }, + "redux-thunk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz", + "integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==" + }, "refractor": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/refractor/-/refractor-2.8.0.tgz", diff --git a/package.json b/package.json index fce346bf8..34a3d9836 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "jest": "^24.5.0", "js-yaml": "^3.13.0", "lint-staged": "^8.1.5", + "lodash.keyby": "^4.6.0", "mini-css-extract-plugin": "^0.5.0", "node-sass": "^4.11.0", "prettier": "1.16.4", @@ -58,8 +59,12 @@ "react": "^16.8.4", "react-dom": "^16.8.4", "react-hot-loader": "^4.8.0", + "react-redux": "^7.0.1", "react-router-dom": "^5.0.0", "react-testing-library": "^6.0.0", + "redux": "^4.0.1", + "redux-mock-store": "^1.5.3", + "redux-thunk": "^2.3.0", "sass-loader": "^7.1.0", "storybook-react-router": "^1.0.4", "style-loader": "^0.23.1", diff --git a/src/actions/pipeline.js b/src/actions/pipeline.js new file mode 100644 index 000000000..3cc116360 --- /dev/null +++ b/src/actions/pipeline.js @@ -0,0 +1,29 @@ +/* +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 { getPipelines } from '../api'; + +export function fetchPipelinesSuccess(data) { + return { + type: 'PIPELINE_FETCH_SUCCESS', + data + }; +} + +export function fetchPipelines() { + return async dispatch => { + const pipelines = await getPipelines(); + dispatch(fetchPipelinesSuccess(pipelines)); + return pipelines; + }; +} diff --git a/src/actions/pipeline.test.js b/src/actions/pipeline.test.js new file mode 100644 index 000000000..805e4f781 --- /dev/null +++ b/src/actions/pipeline.test.js @@ -0,0 +1,40 @@ +/* +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 configureStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; + +import * as API from '../api'; +import { fetchPipelines, fetchPipelinesSuccess } from './pipeline'; + +it('fetchPipelinesSuccess', () => { + const data = { fake: 'data' }; + expect(fetchPipelinesSuccess(data)).toEqual({ + type: 'PIPELINE_FETCH_SUCCESS', + data + }); +}); + +it('fetchPipelines', async () => { + const pipelines = { fake: 'pipelines' }; + const middleware = [thunk]; + const mockStore = configureStore(middleware); + const store = mockStore(); + + jest.spyOn(API, 'getPipelines').mockImplementation(() => pipelines); + + const expectedActions = [fetchPipelinesSuccess(pipelines)]; + + await store.dispatch(fetchPipelines()); + expect(store.getActions()).toEqual(expectedActions); +}); diff --git a/src/containers/App/App.test.js b/src/containers/App/App.test.js index 1b4834cf4..1015e0b15 100644 --- a/src/containers/App/App.test.js +++ b/src/containers/App/App.test.js @@ -12,16 +12,24 @@ limitations under the License. */ import React from 'react'; +import { Provider } from 'react-redux'; import { render } from 'react-testing-library'; +import configureStore from 'redux-mock-store'; import App from './App'; import * as API from '../../api'; it('App renders successfully', () => { + const mockStore = configureStore(); + const store = mockStore(); const getPipelines = jest .spyOn(API, 'getPipelines') .mockImplementation(() => []); - const { queryByText } = render(); + const { queryByText } = render( + + + + ); expect(queryByText(/pipelines/i)).toBeTruthy(); expect(getPipelines).toHaveBeenCalledTimes(1); }); diff --git a/src/containers/Pipelines/Pipelines.js b/src/containers/Pipelines/Pipelines.js index 918ca21d4..38de36d8b 100644 --- a/src/containers/Pipelines/Pipelines.js +++ b/src/containers/Pipelines/Pipelines.js @@ -13,6 +13,7 @@ limitations under the License. import React, { Component } from 'react'; import { Link } from 'react-router-dom'; +import { connect } from 'react-redux'; import { Breadcrumb, BreadcrumbItem, @@ -27,11 +28,12 @@ import { import Header from '../../components/Header'; import { getPipelines } from '../../api'; +import { fetchPipelines } from '../../actions/pipeline'; import '../../components/Pipelines/Pipelines.scss'; /* istanbul ignore next */ -class Pipelines extends Component { +export class Pipelines extends Component { state = { error: null, loading: true, @@ -109,4 +111,7 @@ class Pipelines extends Component { } } -export default Pipelines; +export default connect( + null, + { fetchPipelines } +)(Pipelines); diff --git a/src/index.js b/src/index.js index d866e4d77..1508cd5b7 100644 --- a/src/index.js +++ b/src/index.js @@ -13,10 +13,17 @@ limitations under the License. import React from 'react'; import ReactDOM from 'react-dom'; +import { Provider } from 'react-redux'; import './utils/polyfills'; +import store from './store'; import App from './containers/App'; /* istanbul ignore next */ -ReactDOM.render(, document.getElementById('root')); +ReactDOM.render( + + + , + document.getElementById('root') +); diff --git a/src/reducers/index.js b/src/reducers/index.js new file mode 100644 index 000000000..316683b7e --- /dev/null +++ b/src/reducers/index.js @@ -0,0 +1,14 @@ +/* +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. +*/ + +export pipeline from './pipeline'; diff --git a/src/reducers/pipeline.js b/src/reducers/pipeline.js new file mode 100644 index 000000000..e8d67d8c9 --- /dev/null +++ b/src/reducers/pipeline.js @@ -0,0 +1,23 @@ +/* +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 keyBy from 'lodash.keyby'; + +export default function pipeline(state = {}, action) { + switch (action.type) { + case 'PIPELINE_FETCH_SUCCESS': + return keyBy(action.data, 'metadata.name'); + default: + return state; + } +} diff --git a/src/reducers/pipeline.test.js b/src/reducers/pipeline.test.js new file mode 100644 index 000000000..643986ff1 --- /dev/null +++ b/src/reducers/pipeline.test.js @@ -0,0 +1,34 @@ +/* +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 pipelineReducer from './pipeline'; + +it('handles init or unknown actions', () => { + expect(pipelineReducer(undefined, { type: 'does_not_exist' })).toEqual({}); +}); + +it('PIPELINE_FETCH_SUCCESS', () => { + const name = 'pipeline name'; + const pipeline = { + metadata: { + name + }, + other: 'content' + }; + const action = { + type: 'PIPELINE_FETCH_SUCCESS', + data: [pipeline] + }; + + expect(pipelineReducer({}, action)).toEqual({ [name]: pipeline }); +}); diff --git a/src/store/index.js b/src/store/index.js new file mode 100644 index 000000000..96efd4ea5 --- /dev/null +++ b/src/store/index.js @@ -0,0 +1,35 @@ +/* +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 { applyMiddleware, combineReducers, compose, createStore } from 'redux'; +import thunk from 'redux-thunk'; + +import * as reducers from '../reducers'; + +/* eslint-disable no-underscore-dangle */ +const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; +/* eslint-enable */ + +export function configureStore(initialState = {}) { + const rootReducer = combineReducers(reducers); + const middleware = [thunk]; + + return createStore( + rootReducer, + initialState, + composeEnhancers(applyMiddleware(...middleware)) + ); +} + +const store = configureStore(); +export default store; From 58422c9f8fe5feaaa9e9052cae9890965b0f574a Mon Sep 17 00:00:00 2001 From: Adam Roberts Date: Wed, 17 Apr 2019 14:12:39 +0100 Subject: [PATCH 2/3] Advocate using ko in developer docs (#55) --- .eslintrc.js | 2 +- DEVELOPMENT.md | 77 ++++++--- Gopkg.lock | 151 ++++++++++++++---- README.md | 3 +- .../tekton-dashboard-deployment.yaml | 3 +- .../__mocks__/fileMock.js | 0 .../__mocks__/styleMock.js | 0 {config => config_frontend}/config.json | 0 {config => config_frontend}/setupTests.js | 0 jest.config.js | 6 +- webpack.dev.js | 2 +- 11 files changed, 183 insertions(+), 61 deletions(-) rename {install => config}/tekton-dashboard-deployment.yaml (89%) rename {config => config_frontend}/__mocks__/fileMock.js (100%) rename {config => config_frontend}/__mocks__/styleMock.js (100%) rename {config => config_frontend}/config.json (100%) rename {config => config_frontend}/setupTests.js (100%) diff --git a/.eslintrc.js b/.eslintrc.js index 078244d37..e32e03859 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -34,7 +34,7 @@ module.exports = { }, "overrides": [ { - "files": ["webpack.*.js", "config/setupTests.js", "*.stories.js"], + "files": ["webpack.*.js", "config_frontend/setupTests.js", "*.stories.js"], "rules": { "import/no-extraneous-dependencies": ["error", { "devDependencies": true }] } diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 23249f343..f935ed83c 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -14,16 +14,18 @@ We would love to accomplish these tasks and to update this document, contributio You must install these tools: -1. [`go`](https://golang.org/doc/install): The language Tekton Dashboard is - built in +1. [`go`](https://golang.org/doc/install): The language Tekton Dashboard is built in 1. [`git`](https://help.github.com/articles/set-up-git/): For source control -1. [`dep`](https://github.com/golang/dep): For managing external Go - dependencies. - Please Install dep v0.5.0 or greater. +1. [`dep`](https://github.com/golang/dep): For managing external Go dependencies. - Please Install dep v0.5.0 or greater. +1. [`ko`](https://github.com/google/ko): For development. `ko` version v0.1 or higher is required for `dashboard` to work correctly. 1. [Node.js & npm](https://nodejs.org/): For building and running the frontend locally. See `engines` in [package.json](./package.json) for versions used. -1. [`kubectl`](https://kubernetes.io/docs/tasks/tools/install-kubectl/): For - interacting with your kube cluster. +1. [`kubectl`](https://kubernetes.io/docs/tasks/tools/install-kubectl/): For interacting with your kube cluster. - Note that there exists a bug in certain versions of `kubectl` whereby the `auth` field is missing from created secrets. Known good versions we've tested are __1.11.3__ and __1.13.2__. +Your [`$GOPATH`] setting is critical for `ko apply` to function properly: a +successful run will typically involve building pushing images instead of only +configuring Kubernetes resources. + +Note that there exists a bug in certain versions of `kubectl` whereby the `auth` field is missing from created secrets. Known good versions we've tested are __1.11.3__ and __1.13.2__. ### Checkout your fork @@ -54,6 +56,44 @@ _Adding the `upstream` remote sets you up nicely for regularly - We've had good success using Docker Desktop: ensure your Kubernetes cluster is healthy and you have plenty of disk space allocated as PVs will be created for PipelineRuns. - Ensure you can push images to a Docker registry - the above listed requirements are only for local development, otherwise we pull in the tooling for you in the image. +1. `GOPATH`: If you don't have one, simply pick a directory and add + `export GOPATH=...` +1. `$GOPATH/bin` on `PATH`: This is so that tooling installed via `go get` will + work properly. +1. `KO_DOCKER_REPO`: The docker repository to which developer images should be + pushed (e.g. `gcr.io/[gcloud-project]` or `docker.io`). You can also run a local registry + and set `KO_DOCKER_REPO` to reference the registry (e.g. at + `localhost:5000/mydashboardimages`). + +`.bashrc` example: + +```shell +export GOPATH="$HOME/go" +export PATH="${PATH}:${GOPATH}/bin" +export KO_DOCKER_REPO='docker.io/myusername' +``` + +Make sure to configure +[authentication](https://cloud.google.com/container-registry/docs/advanced-authentication#standalone_docker_credential_helper) +for your `KO_DOCKER_REPO` if required. To be able to push images to +`gcr.io/`, you need to run this once: + +```shell +gcloud auth configure-docker +``` + +The user you are using to interact with your k8s cluster must be a cluster admin +to create role bindings: + +```shell +# Using gcloud to get your current user +USER=$(gcloud config get-value core/account) +# Make that user a cluster admin +kubectl create clusterrolebinding cluster-admin-binding \ + --clusterrole=cluster-admin \ + --user="${USER}" +``` + ### Namespaces Currently you must install the Tekton dashboard into the same namespace you wish to create and get Tekton resources in. @@ -62,9 +102,8 @@ Currently you must install the Tekton dashboard into the same namespace you wish While iterating on the project, you may need to: -1. Docker build and push your image of the dashboard -1. Run the Go tests with: `docker build -f Dockerfile_test .` -1. Replace the `image` reference in the yaml located in the `install` folder to reference your built and pushed image's location +1. Run `dep ensure -v` to retrieve dependencies required to build +1. Run the Go tests in Docker with: `docker build -f Dockerfile_test .` 1. Install the dashboard 1. Interact with the created Kubernetes service - we've had success using Postman on Mac and data provided must be JSON @@ -72,11 +111,11 @@ Tekton Dashboard does not involve any custom resource definitions, we only inter ## Install dashboard -After you've built and pushed the image, and modified the `install` yaml to refer to your image, you can stand up a version of the dashboard on-cluster (to your +You can stand up a version of the dashboard on-cluster (to your `kubectl config current-context`): ```shell -kubectl apply -f `install` +ko apply -f config/ ``` ## Access the dashboard @@ -89,18 +128,14 @@ Note that we have a big TODO which is to link up the frontend to the backend and ### Redeploy dashboard -As you make changes to the code, you can redeploy your dashboard by killing the pod and so the new container code will be used if it has been pushed. The pod is labelled with `tekton-dashboard` so you can do: - -```shell -kubectl delete pod -l app=tekton-dashboard -``` +As you make changes to the code, you can redeploy your dashboard by simply using `ko apply` against the `config` directory again. ### Tear it down -You can remove the deployment and any pods that were created with: +You can clean up everything with: ```shell -kubectl delete deployment -l app=tekton-dashboard-deployment +ko delete -f config/ ``` ## Accessing logs @@ -123,7 +158,7 @@ npm install Run `npm start` for a dev server. Navigate to `http://localhost:8000/` in your browser. The app will automatically hot-reload any changes to the source files, including CSS. If it is unable to hot-reload it will fallback to a full page refresh. -Note: If you've exposed the backend by some other means than port-forwarding port 9097 as described above, update `API_DOMAIN` in `config/config.json` to provide the correct details. +Note: If you've exposed the backend by some other means than port-forwarding port 9097 as described above, update `API_DOMAIN` in `config_frontend/config.json` to provide the correct details. ### Build @@ -362,4 +397,4 @@ Delete a credential by ID Returns HTTP code 200 if the credential was deleted Returns HTTP code 400 if a bad request was used or if the secret was not found Returns HTTP code 500 if the found credential could not be deleted -``` +``` \ No newline at end of file diff --git a/Gopkg.lock b/Gopkg.lock index 8b19ef1a8..fe6a31ad9 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -2,195 +2,232 @@ [[projects]] + digest = "1:ffe9824d294da03b391f44e1ae8281281b4afc1bdaa9588c9097785e3af10cec" name = "github.com/davecgh/go-spew" packages = ["spew"] + pruneopts = "UT" revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73" version = "v1.1.1" [[projects]] + digest = "1:4407e93a89da65e7f7e51768c1a4b20ba035e463f78b3027b791dd4f29174ced" name = "github.com/emicklei/go-restful" packages = [ ".", - "log" + "log", ] + pruneopts = "UT" revision = "b9bbc5664f49b6deec52393bd68f39830687a347" version = "v2.9.3" [[projects]] + digest = "1:f1f2bd73c025d24c3b93abf6364bccb802cf2fdedaa44360804c67800e8fab8d" name = "github.com/evanphx/json-patch" packages = ["."] + pruneopts = "UT" revision = "72bf35d0ff611848c1dc9df0f976c81192392fa5" version = "v4.1.0" [[projects]] + digest = "1:2cd7915ab26ede7d95b8749e6b1f933f1c6d5398030684e6505940a10f31cfda" name = "github.com/ghodss/yaml" packages = ["."] + pruneopts = "UT" revision = "0ca9ea5df5451ffdf184b4428c902747c2c11cd7" version = "v1.0.0" [[projects]] + digest = "1:4d02824a56d268f74a6b6fdd944b20b58a77c3d70e81008b3ee0c4f1a6777340" name = "github.com/gogo/protobuf" packages = [ "proto", - "sortkeys" + "sortkeys", ] + pruneopts = "UT" revision = "ba06b47c162d49f2af050fb4c75bcbc86a159d5c" version = "v1.2.1" [[projects]] branch = "master" + digest = "1:1ba1d79f2810270045c328ae5d674321db34e3aae468eb4233883b473c5c0467" name = "github.com/golang/glog" packages = ["."] + pruneopts = "UT" revision = "23def4e6c14b4da8ac2ed8007337bc5eb5007998" [[projects]] + digest = "1:239c4c7fd2159585454003d9be7207167970194216193a8a210b8d29576f19c9" name = "github.com/golang/protobuf" packages = [ "proto", "ptypes", "ptypes/any", "ptypes/duration", - "ptypes/timestamp" + "ptypes/timestamp", ] + pruneopts = "UT" revision = "b5d812f8a3706043e23a9cd5babf2e5423744d30" version = "v1.3.1" [[projects]] + digest = "1:0bfbe13936953a98ae3cfe8ed6670d396ad81edf069a806d2f6515d7bb6950df" name = "github.com/google/btree" packages = ["."] + pruneopts = "UT" revision = "4030bb1f1f0c35b30ca7009e9ebd06849dd45306" version = "v1.0.0" [[projects]] + digest = "1:2e3c336fc7fde5c984d2841455a658a6d626450b1754a854b3b32e7a8f49a07a" name = "github.com/google/go-cmp" packages = [ "cmp", "cmp/internal/diff", "cmp/internal/function", - "cmp/internal/value" + "cmp/internal/value", ] + pruneopts = "UT" revision = "3af367b6b30c263d47e8895973edcca9a49cf029" version = "v0.2.0" [[projects]] + digest = "1:a6181aca1fd5e27103f9a920876f29ac72854df7345a39f3b01e61c8c94cc8af" name = "github.com/google/gofuzz" packages = ["."] + pruneopts = "UT" revision = "f140a6486e521aad38f5917de355cbf147cc0496" version = "v1.0.0" [[projects]] + digest = "1:65c4414eeb350c47b8de71110150d0ea8a281835b1f386eacaa3ad7325929c21" name = "github.com/googleapis/gnostic" packages = [ "OpenAPIv2", "compiler", - "extensions" + "extensions", ] + pruneopts = "UT" revision = "7c663266750e7d82587642f65e60bc4083f1f84e" version = "v0.2.0" [[projects]] + digest = "1:7b5c6e2eeaa9ae5907c391a91c132abfd5c9e8a784a341b5625e750c67e6825d" name = "github.com/gorilla/websocket" packages = ["."] + pruneopts = "UT" revision = "66b9c49e59c6c48f0ffce28c2d8b8a5678502c6d" version = "v1.4.0" [[projects]] branch = "master" + digest = "1:b4395b2a4566c24459af3d04009b39cc21762fc77ec7bf7a1aa905c91e8f018d" name = "github.com/gregjones/httpcache" packages = [ ".", - "diskcache" + "diskcache", ] + pruneopts = "UT" revision = "3befbb6ad0cc97d4c25d851e9528915809e1a22f" [[projects]] + digest = "1:d15ee511aa0f56baacc1eb4c6b922fa1c03b38413b6be18166b996d82a0156ea" name = "github.com/hashicorp/golang-lru" packages = [ ".", - "simplelru" + "simplelru", ] + pruneopts = "UT" revision = "7087cb70de9f7a8bc0a10c375cb0d2280a8edf9c" version = "v0.5.1" [[projects]] + digest = "1:a0cefd27d12712af4b5018dc7046f245e1e3b5760e2e848c30b171b570708f9b" name = "github.com/imdario/mergo" packages = ["."] + pruneopts = "UT" revision = "7c29201646fa3de8506f701213473dd407f19646" version = "v0.3.7" [[projects]] + digest = "1:f5a2051c55d05548d2d4fd23d244027b59fbd943217df8aa3b5e170ac2fd6e1b" name = "github.com/json-iterator/go" packages = ["."] + pruneopts = "UT" revision = "0ff49de124c6f76f8494e194af75bde0f1a49a29" version = "v1.1.6" [[projects]] branch = "master" + digest = "1:58dc2abfa0d3901dde142cd636abf13d495f37c125564390c7a5791d1fa29e80" name = "github.com/knative/pkg" packages = [ "apis", "apis/duck", "apis/duck/v1alpha1", - "kmp" + "kmp", ] + pruneopts = "UT" revision = "28cfa161499b88cc5a71cfb7cf8a59fc34bff3d3" [[projects]] branch = "master" + digest = "1:fc2b04b0069d6b10bdef96d278fe20c345794009685ed3c8c7f1a6dc023eefec" name = "github.com/mattbaird/jsonpatch" packages = ["."] + pruneopts = "UT" revision = "81af80346b1a01caae0cbc27fd3c1ba5b11e189f" [[projects]] + digest = "1:33422d238f147d247752996a26574ac48dcf472976eda7f5134015f06bf16563" name = "github.com/modern-go/concurrent" packages = ["."] + pruneopts = "UT" revision = "bacd9c7ef1dd9b15be4a9909b8ac7a4e313eec94" version = "1.0.3" [[projects]] + digest = "1:e32bdbdb7c377a07a9a46378290059822efdce5c8d96fe71940d87cb4f918855" name = "github.com/modern-go/reflect2" packages = ["."] + pruneopts = "UT" revision = "4b7aa43c6742a2c18fdef89dd197aaae7dac7ccd" version = "1.0.1" [[projects]] branch = "master" + digest = "1:3bf17a6e6eaa6ad24152148a631d18662f7212e21637c2699bff3369b7f00fa2" name = "github.com/petar/GoLLRB" packages = ["llrb"] + pruneopts = "UT" revision = "53be0d36a84c2a886ca057d34b6aa4468df9ccb4" [[projects]] + digest = "1:0e7775ebbcf00d8dd28ac663614af924411c868dca3d5aa762af0fae3808d852" name = "github.com/peterbourgon/diskv" packages = ["."] + pruneopts = "UT" revision = "5f041e8faa004a95c88a202771f4cc3e991971e6" version = "v2.0.1" [[projects]] + digest = "1:274f67cb6fed9588ea2521ecdac05a6d62a8c51c074c1fccc6a49a40ba80e925" name = "github.com/satori/go.uuid" packages = ["."] + pruneopts = "UT" revision = "f58768cc1a7a7e77a3bd49e98cdd21419399b6a3" version = "v1.2.0" [[projects]] + digest = "1:c1b1102241e7f645bc8e0c22ae352e8f0dc6484b6cb4d132fa9f24174e0119e2" name = "github.com/spf13/pflag" packages = ["."] + pruneopts = "UT" revision = "298182f68c66c05229eb03ac171abe6e309ee79a" version = "v1.0.3" [[projects]] branch = "master" - name = "github.com/tektoncd/dashboard" - packages = [ - "pkg/broadcaster", - "pkg/endpoints", - "pkg/logging", - "pkg/utils", - "pkg/websocket" - ] - revision = "ecc9da8a0c1d069cc89eddc21fbd1c7f948f8a6a" - -[[projects]] - branch = "master" + digest = "1:aff35c0379f09633e57e10fcd3a00e7563a2669c668e184fe3e2f94a5915f028" name = "github.com/tektoncd/pipeline" packages = [ "pkg/apis/pipeline", @@ -207,23 +244,29 @@ "pkg/client/listers/pipeline/v1alpha1", "pkg/list", "pkg/names", - "pkg/templating" + "pkg/templating", ] + pruneopts = "UT" revision = "c463f1230adc340ee96c02c60f074fff83240210" [[projects]] + digest = "1:3c1a69cdae3501bf75e76d0d86dc6f2b0a7421bc205c0cb7b96b19eed464a34d" name = "go.uber.org/atomic" packages = ["."] + pruneopts = "UT" revision = "1ea20fb1cbb1cc08cbd0d913a96dead89aa18289" version = "v1.3.2" [[projects]] + digest = "1:60bf2a5e347af463c42ed31a493d817f8a72f102543060ed992754e689805d1a" name = "go.uber.org/multierr" packages = ["."] + pruneopts = "UT" revision = "3c4937480c32f4c13a875a1829af76c98ca3d40a" version = "v1.1.0" [[projects]] + digest = "1:c52caf7bd44f92e54627a31b85baf06a68333a196b3d8d241480a774733dcf8b" name = "go.uber.org/zap" packages = [ ".", @@ -231,19 +274,23 @@ "internal/bufferpool", "internal/color", "internal/exit", - "zapcore" + "zapcore", ] + pruneopts = "UT" revision = "ff33455a0e382e8a81d14dd7c922020b6b5e7982" version = "v1.9.1" [[projects]] branch = "master" + digest = "1:bbe51412d9915d64ffaa96b51d409e070665efc5194fcf145c4a27d4133107a4" name = "golang.org/x/crypto" packages = ["ssh/terminal"] + pruneopts = "UT" revision = "38d8ce5564a5b71b2e3a00553993f1b9a7ae852f" [[projects]] branch = "master" + digest = "1:c745644c88df7496196bf316dd0e813f1908215ff29e96622c2fc194f21bac0c" name = "golang.org/x/net" packages = [ "context", @@ -251,29 +298,35 @@ "http/httpguts", "http2", "http2/hpack", - "idna" + "idna", ] + pruneopts = "UT" revision = "eb5bcb51f2a31c7d5141d810b70815c05d9c9146" [[projects]] branch = "master" + digest = "1:9927d6aceb89d188e21485f42a7a254e67e6fdcf4260aba375fe18e3c300dfb4" name = "golang.org/x/oauth2" packages = [ ".", - "internal" + "internal", ] + pruneopts = "UT" revision = "9f3314589c9a9136388751d9adae6b0ed400978a" [[projects]] branch = "master" + digest = "1:503f66aac79cb9135d73679c7fa3e95a6fad82a3da399c505ea282b50ff03723" name = "golang.org/x/sys" packages = [ "unix", - "windows" + "windows", ] + pruneopts = "UT" revision = "4b34438f7a67ee5f45cc6132e2bad873a20324e9" [[projects]] + digest = "1:a2ab62866c75542dd18d2b069fec854577a20211d7c0ea6ae746072a1dccdd18" name = "golang.org/x/text" packages = [ "collate", @@ -289,18 +342,22 @@ "unicode/bidi", "unicode/cldr", "unicode/norm", - "unicode/rangetable" + "unicode/rangetable", ] + pruneopts = "UT" revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" version = "v0.3.0" [[projects]] branch = "master" + digest = "1:9fdc2b55e8e0fafe4b41884091e51e77344f7dc511c5acedcfd98200003bff90" name = "golang.org/x/time" packages = ["rate"] + pruneopts = "UT" revision = "9d24e82272b4f38b78bc8cff74fa936d31ccd8ef" [[projects]] + digest = "1:6eb6e3b6d9fffb62958cf7f7d88dbbe1dd6839436b0802e194c590667a40412a" name = "google.golang.org/appengine" packages = [ "internal", @@ -309,24 +366,30 @@ "internal/log", "internal/remote_api", "internal/urlfetch", - "urlfetch" + "urlfetch", ] + pruneopts = "UT" revision = "54a98f90d1c46b7731eb8fb305d2a321c30ef610" version = "v1.5.0" [[projects]] + digest = "1:2d1fbdc6777e5408cabeb02bf336305e724b925ff4546ded0fa8715a7267922a" name = "gopkg.in/inf.v0" packages = ["."] + pruneopts = "UT" revision = "d2d2541c53f18d2a059457998ce2876cc8e67cbf" version = "v0.9.1" [[projects]] + digest = "1:4d2e5a73dc1500038e504a8d78b986630e3626dc027bc030ba5c75da257cdb96" name = "gopkg.in/yaml.v2" packages = ["."] + pruneopts = "UT" revision = "51d6538a90f86fe93ac480b35f37b2be17fef232" version = "v2.2.2" [[projects]] + digest = "1:faf52d216e0d651ee48a5ee8fb43f818a6c36bf4246282a5e30b8985da5581ff" name = "k8s.io/api" packages = [ "admissionregistration/v1alpha1", @@ -359,12 +422,14 @@ "settings/v1alpha1", "storage/v1", "storage/v1alpha1", - "storage/v1beta1" + "storage/v1beta1", ] + pruneopts = "UT" revision = "145d52631d00cbfe68490d19ae4f0f501fd31a95" version = "kubernetes-1.12.6" [[projects]] + digest = "1:94babe94a374f3367bc9606841879a72591bef89a7370751b0cc3957f0d8c804" name = "k8s.io/apimachinery" packages = [ "pkg/api/equality", @@ -412,12 +477,14 @@ "pkg/version", "pkg/watch", "third_party/forked/golang/json", - "third_party/forked/golang/reflect" + "third_party/forked/golang/reflect", ] + pruneopts = "UT" revision = "01f179d85dbce0f2e0e4351a92394b38694b7cae" version = "kubernetes-1.12.6" [[projects]] + digest = "1:38b619740b74c3596fce093a1f6cf70245259797538454662ff3973067a80e2b" name = "k8s.io/client-go" packages = [ "discovery", @@ -512,26 +579,48 @@ "util/flowcontrol", "util/homedir", "util/integer", - "util/retry" + "util/retry", ] + pruneopts = "UT" revision = "78295b709ec6fa5be12e35892477a326dea2b5d3" version = "kubernetes-1.12.6" [[projects]] branch = "master" + digest = "1:22abb5d4204ab1a0dcc9cda64906a31c43965ff5159e8b9f766c9d2a162dbed5" name = "k8s.io/kube-openapi" packages = ["pkg/util/proto"] + pruneopts = "UT" revision = "94e1e7b7574c44c4c0f2007de6fe617e259191f3" [[projects]] branch = "master" + digest = "1:e1fc4a624e8497252148125a50cec64b416d69ef69afc2244498e0c9b73060d5" name = "k8s.io/sample-controller" packages = ["pkg/signals"] + pruneopts = "UT" revision = "ef739b027d6ca5a347fdba88dec315e56ee14b2e" [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "52b8a6d95fec414aaa0086a16165a3b7d01dbfa130037f98a9e1ccc03c3fc324" + input-imports = [ + "github.com/emicklei/go-restful", + "github.com/gorilla/websocket", + "github.com/satori/go.uuid", + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1", + "github.com/tektoncd/pipeline/pkg/client/clientset/versioned", + "github.com/tektoncd/pipeline/pkg/client/clientset/versioned/fake", + "github.com/tektoncd/pipeline/pkg/client/informers/externalversions", + "go.uber.org/zap", + "k8s.io/api/core/v1", + "k8s.io/apimachinery/pkg/apis/meta/v1", + "k8s.io/client-go/kubernetes", + "k8s.io/client-go/kubernetes/fake", + "k8s.io/client-go/rest", + "k8s.io/client-go/tools/cache", + "k8s.io/client-go/tools/clientcmd", + "k8s.io/sample-controller/pkg/signals", + ] solver-name = "gps-cdcl" solver-version = 1 diff --git a/README.md b/README.md index 41656dc41..d2f2677e9 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,7 @@ $ kubectl port-forward $(kubectl get pod -l app=tekton-dashboard -o name) 9097:9 ``` **Coming soon** -- All API definitions! -- Deploying the dashboard without using the install yaml, and accessing using kubectl proxy. To deploy the dashboard, execute the following command: +- Deploying the dashboard without using the `config/` yaml, and accessing using kubectl proxy. To deploy the dashboard, execute the following command: ```sh $ kubectl apply -f https://raw.githubusercontent.com/tektoncd/dashboard/... diff --git a/install/tekton-dashboard-deployment.yaml b/config/tekton-dashboard-deployment.yaml similarity index 89% rename from install/tekton-dashboard-deployment.yaml rename to config/tekton-dashboard-deployment.yaml index 79ebf3fa7..98bad4874 100644 --- a/install/tekton-dashboard-deployment.yaml +++ b/config/tekton-dashboard-deployment.yaml @@ -17,8 +17,7 @@ spec: spec: containers: - name: tekton-dashboard - image: "CHANGE_ME/back-end:latest" - imagePullPolicy: Always + image: github.com/tektoncd/dashboard/cmd/dashboard ports: - containerPort: 9097 livenessProbe: diff --git a/config/__mocks__/fileMock.js b/config_frontend/__mocks__/fileMock.js similarity index 100% rename from config/__mocks__/fileMock.js rename to config_frontend/__mocks__/fileMock.js diff --git a/config/__mocks__/styleMock.js b/config_frontend/__mocks__/styleMock.js similarity index 100% rename from config/__mocks__/styleMock.js rename to config_frontend/__mocks__/styleMock.js diff --git a/config/config.json b/config_frontend/config.json similarity index 100% rename from config/config.json rename to config_frontend/config.json diff --git a/config/setupTests.js b/config_frontend/setupTests.js similarity index 100% rename from config/setupTests.js rename to config_frontend/setupTests.js diff --git a/jest.config.js b/jest.config.js index 4a9d7299d..283cb022a 100644 --- a/jest.config.js +++ b/jest.config.js @@ -27,10 +27,10 @@ module.exports = { } }, moduleNameMapper: { - '\\.(png|svg|ttf|woff|woff2)$': '/config/__mocks__/fileMock.js', - '\\.(css|scss)$': '/config/__mocks__/styleMock.js' + '\\.(png|svg|ttf|woff|woff2)$': '/config_frontend/__mocks__/fileMock.js', + '\\.(css|scss)$': '/config_frontend/__mocks__/styleMock.js' }, - setupFilesAfterEnv: ['/config/setupTests.js'], + setupFilesAfterEnv: ['/config_frontend/setupTests.js'], testMatch: ['/src/**/*.test.js'], testPathIgnorePatterns: ['node_modules'] }; diff --git a/webpack.dev.js b/webpack.dev.js index 8116e1f23..263a1d06b 100644 --- a/webpack.dev.js +++ b/webpack.dev.js @@ -1,7 +1,7 @@ const merge = require('webpack-merge'); const common = require('./webpack.common.js'); -const { API_DOMAIN, PORT } = require('./config/config.json'); +const { API_DOMAIN, PORT } = require('./config_frontend/config.json'); module.exports = merge(common, { mode: 'development', From e5ff8beb51a49e4bdb47dd5b9354347c5a04ca09 Mon Sep 17 00:00:00 2001 From: Steve O'Donovan Date: Wed, 17 Apr 2019 14:24:48 +0100 Subject: [PATCH 3/3] Add tasks as a top level navigation (#49) --- .../Definitions.scss} | 6 +- .../PipelineRunHeader.stories.js | 56 ----- .../PipelineRun.scss => Run/Run.scss} | 6 +- .../RunHeader.js} | 28 +-- .../RunHeader.scss} | 0 src/components/RunHeader/RunHeader.stories.js | 64 ++++++ .../RunHeader.test.js} | 26 +-- .../{PipelineRunHeader => RunHeader}/index.js | 2 +- src/components/Task/Task.js | 2 +- src/components/Task/Task.scss | 2 + src/components/TaskTree/TaskTree.stories.js | 26 +-- src/containers/App/App.js | 15 +- src/containers/App/App.test.js | 2 +- src/containers/Home/Home.js | 67 ++++++ src/containers/Home/index.js | 14 ++ src/containers/PipelineRun/PipelineRun.js | 97 +++------ src/containers/PipelineRuns/PipelineRuns.js | 10 +- src/containers/Pipelines/Pipelines.js | 8 +- src/containers/TaskRuns/TaskRuns.js | 194 ++++++++++++++++++ src/containers/TaskRuns/TaskRuns.test.js | 76 +++++++ src/containers/TaskRuns/index.js | 14 ++ src/containers/Tasks/Tasks.js | 109 ++++++++++ src/containers/Tasks/index.js | 14 ++ src/containers/index.js | 3 + src/utils/index.js | 56 ++++- src/utils/index.test.js | 137 ++++++++++++- 26 files changed, 846 insertions(+), 188 deletions(-) rename src/components/{Pipelines/Pipelines.scss => Definitions/Definitions.scss} (95%) delete mode 100644 src/components/PipelineRunHeader/PipelineRunHeader.stories.js rename src/components/{PipelineRun/PipelineRun.scss => Run/Run.scss} (92%) rename src/components/{PipelineRunHeader/PipelineRunHeader.js => RunHeader/RunHeader.js} (79%) rename src/components/{PipelineRunHeader/PipelineRunHeader.scss => RunHeader/RunHeader.scss} (100%) create mode 100644 src/components/RunHeader/RunHeader.stories.js rename src/components/{PipelineRunHeader/PipelineRunHeader.test.js => RunHeader/RunHeader.test.js} (59%) rename src/components/{PipelineRunHeader => RunHeader}/index.js (92%) create mode 100644 src/containers/Home/Home.js create mode 100644 src/containers/Home/index.js create mode 100644 src/containers/TaskRuns/TaskRuns.js create mode 100644 src/containers/TaskRuns/TaskRuns.test.js create mode 100644 src/containers/TaskRuns/index.js create mode 100644 src/containers/Tasks/Tasks.js create mode 100644 src/containers/Tasks/index.js diff --git a/src/components/Pipelines/Pipelines.scss b/src/components/Definitions/Definitions.scss similarity index 95% rename from src/components/Pipelines/Pipelines.scss rename to src/components/Definitions/Definitions.scss index a4a6eb295..3aa1654b1 100644 --- a/src/components/Pipelines/Pipelines.scss +++ b/src/components/Definitions/Definitions.scss @@ -13,8 +13,8 @@ limitations under the License. @import '../../scss/vars'; -.pipelines { - .pipelines-header { +.definitions { + .definitions-header { background: white; padding-bottom: 1em; @@ -36,7 +36,7 @@ limitations under the License. padding: 2em 5%; } - .pipeline .status { + .definition .status { .status-icon { margin-right: 0.5em; vertical-align: sub; diff --git a/src/components/PipelineRunHeader/PipelineRunHeader.stories.js b/src/components/PipelineRunHeader/PipelineRunHeader.stories.js deleted file mode 100644 index fb3c3f3c2..000000000 --- a/src/components/PipelineRunHeader/PipelineRunHeader.stories.js +++ /dev/null @@ -1,56 +0,0 @@ -/* -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 { text } from '@storybook/addon-knobs'; -import StoryRouter from 'storybook-react-router'; - -import PipelineRunHeader from './PipelineRunHeader'; - -storiesOf('PipelineRunHeader', module) - .addDecorator(StoryRouter()) - .add('default', () => ( - - )) - .add('running', () => ( - - )) - .add('complete', () => ( - - )) - .add('failed', () => ( - - )) - .add('error', () => ( - - )) - .add('loading', () => ); diff --git a/src/components/PipelineRun/PipelineRun.scss b/src/components/Run/Run.scss similarity index 92% rename from src/components/PipelineRun/PipelineRun.scss rename to src/components/Run/Run.scss index f0e920ae4..b7437095a 100644 --- a/src/components/PipelineRun/PipelineRun.scss +++ b/src/components/Run/Run.scss @@ -11,7 +11,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -.pipeline-run { +.run { main { padding: 2em 5%; } @@ -33,4 +33,8 @@ limitations under the License. .step-details { flex-grow: 1; } + + .bx--inline-notification__close-button{ + visibility: hidden; + } } diff --git a/src/components/PipelineRunHeader/PipelineRunHeader.js b/src/components/RunHeader/RunHeader.js similarity index 79% rename from src/components/PipelineRunHeader/PipelineRunHeader.js rename to src/components/RunHeader/RunHeader.js index df25ce67a..5a6c45c13 100644 --- a/src/components/PipelineRunHeader/PipelineRunHeader.js +++ b/src/components/RunHeader/RunHeader.js @@ -22,20 +22,22 @@ import { import { Link } from 'react-router-dom'; import Header from '../Header'; -import { getPipelineRunStatusIcon } from '../../utils'; +import { getStatusIcon } from '../../utils'; -import './PipelineRunHeader.scss'; +import './RunHeader.scss'; -class PipelineRunHeader extends Component { +class RunHeader extends Component { render() { const { error, lastTransitionTime, loading, - pipelineName, - pipelineRunName, + name, + runName, reason, - status + status, + type, + typeLabel } = this.props; return ( @@ -47,11 +49,11 @@ class PipelineRunHeader extends Component { > - Pipelines + {typeLabel} - {pipelineName && ( + {name && ( - {pipelineName} + {name} )} @@ -64,12 +66,12 @@ class PipelineRunHeader extends Component { return ; } return ( - pipelineRunName && ( + runName && (

- {getPipelineRunStatusIcon({ reason, status })} + {getStatusIcon({ reason, status })}
- {pipelineRunName} + {runName} {reason} {lastTransitionTime}

@@ -89,4 +91,4 @@ class PipelineRunHeader extends Component { } } -export default PipelineRunHeader; +export default RunHeader; diff --git a/src/components/PipelineRunHeader/PipelineRunHeader.scss b/src/components/RunHeader/RunHeader.scss similarity index 100% rename from src/components/PipelineRunHeader/PipelineRunHeader.scss rename to src/components/RunHeader/RunHeader.scss diff --git a/src/components/RunHeader/RunHeader.stories.js b/src/components/RunHeader/RunHeader.stories.js new file mode 100644 index 000000000..37e7c191f --- /dev/null +++ b/src/components/RunHeader/RunHeader.stories.js @@ -0,0 +1,64 @@ +/* +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 { text } from '@storybook/addon-knobs'; +import StoryRouter from 'storybook-react-router'; + +import RunHeader from './RunHeader'; + +storiesOf('RunHeader', module) + .addDecorator(StoryRouter()) + .add('default', () => ( + + )) + .add('running', () => ( + + )) + .add('complete', () => ( + + )) + .add('failed', () => ( + + )) + .add('error', () => ( + + )) + .add('loading', () => ); diff --git a/src/components/PipelineRunHeader/PipelineRunHeader.test.js b/src/components/RunHeader/RunHeader.test.js similarity index 59% rename from src/components/PipelineRunHeader/PipelineRunHeader.test.js rename to src/components/RunHeader/RunHeader.test.js index a204711fb..c47de209d 100644 --- a/src/components/PipelineRunHeader/PipelineRunHeader.test.js +++ b/src/components/RunHeader/RunHeader.test.js @@ -12,44 +12,44 @@ limitations under the License. */ import React from 'react'; -import PipelineRunHeader from './PipelineRunHeader'; +import RunHeader from './RunHeader'; import { renderWithRouter } from '../../utils/test'; const props = { - pipelineName: 'simple-pipeline', - pipelineRunName: 'simple-pipeline-run-1' + name: 'simple-pipeline', + runName: 'simple-pipeline-run-1' }; -it('PipelineRunHeader renders the provided content', () => { - const { queryByText } = renderWithRouter(); +it('RunHeader renders the provided content', () => { + const { queryByText } = renderWithRouter(); expect(queryByText(/simple-pipeline/i)).toBeTruthy(); }); -it('PipelineRunHeader renders the running state', () => { +it('RunHeader renders the running state', () => { const { queryByText } = renderWithRouter( - + ); expect(queryByText(/running/i)).toBeTruthy(); }); -it('PipelineRunHeader renders the completed state', () => { +it('RunHeader renders the completed state', () => { const { queryByText } = renderWithRouter( - + ); expect(queryByText(/completed/i)).toBeTruthy(); }); -it('PipelineRunHeader renders the failed state', () => { +it('RunHeader renders the failed state', () => { const { queryByText } = renderWithRouter( - + ); expect(queryByText(/failed/i)).toBeTruthy(); }); -it('PipelineRunHeader renders the pending state', () => { +it('RunHeader renders the pending state', () => { const { queryByText } = renderWithRouter( - + ); expect(queryByText(/an error message/i)).toBeTruthy(); }); diff --git a/src/components/PipelineRunHeader/index.js b/src/components/RunHeader/index.js similarity index 92% rename from src/components/PipelineRunHeader/index.js rename to src/components/RunHeader/index.js index d87ad3dc6..b6d54369a 100644 --- a/src/components/PipelineRunHeader/index.js +++ b/src/components/RunHeader/index.js @@ -11,4 +11,4 @@ See the License for the specific language governing permissions and limitations under the License. */ -export { default } from './PipelineRunHeader'; +export { default } from './RunHeader'; diff --git a/src/components/Task/Task.js b/src/components/Task/Task.js index 48d93d6dc..610a540e4 100644 --- a/src/components/Task/Task.js +++ b/src/components/Task/Task.js @@ -61,7 +61,7 @@ class Task extends Component { const icon = this.icon(); return (
  • - + {icon} {pipelineTaskName} diff --git a/src/components/Task/Task.scss b/src/components/Task/Task.scss index e972b3a15..7b86de20c 100644 --- a/src/components/Task/Task.scss +++ b/src/components/Task/Task.scss @@ -74,6 +74,8 @@ limitations under the License. white-space: nowrap; font-size: 0.76rem; letter-spacing: 0.06rem; + overflow: hidden; + text-overflow: ellipsis; &:after { content: ''; diff --git a/src/components/TaskTree/TaskTree.stories.js b/src/components/TaskTree/TaskTree.stories.js index 592846206..8051a9e12 100644 --- a/src/components/TaskTree/TaskTree.stories.js +++ b/src/components/TaskTree/TaskTree.stories.js @@ -13,23 +13,23 @@ limitations under the License. import React, { Component } from 'react'; import { storiesOf } from '@storybook/react'; +import { text } from '@storybook/addon-knobs'; import TaskTree from './TaskTree'; -const props = { - taskRuns: [ - { - id: 'task', - pipelineTaskName: 'A Task', - steps: [ - { id: 'build', stepName: 'build' }, - { id: 'test', stepName: 'test' } - ] - } - ] -}; - storiesOf('TaskTree', module).add('default', () => { + const props = { + taskRuns: [ + { + id: 'task', + pipelineTaskName: text('Task name', 'default task name'), + steps: [ + { id: 'build', stepName: 'build' }, + { id: 'test', stepName: 'test' } + ] + } + ] + }; class TaskTreeWrapper extends Component { state = { selectedTaskId: null }; diff --git a/src/containers/App/App.js b/src/containers/App/App.js index 609717f94..a20f10246 100644 --- a/src/containers/App/App.js +++ b/src/containers/App/App.js @@ -20,26 +20,35 @@ import { Switch } from 'react-router-dom'; -import { PipelineRun, PipelineRuns, Pipelines } from '..'; +import { + Home, + PipelineRun, + PipelineRuns, + Pipelines, + Tasks, + TaskRuns +} from '..'; import '../../components/App/App.scss'; const App = () => ( - + - + + + { ); expect(queryByText(/pipelines/i)).toBeTruthy(); - expect(getPipelines).toHaveBeenCalledTimes(1); + expect(queryByText(/tasks/i)).toBeTruthy(); }); diff --git a/src/containers/Home/Home.js b/src/containers/Home/Home.js new file mode 100644 index 000000000..fbfbbe312 --- /dev/null +++ b/src/containers/Home/Home.js @@ -0,0 +1,67 @@ +/* +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 { Link } from 'react-router-dom'; +import { + Breadcrumb, + BreadcrumbItem, + StructuredListBody, + StructuredListCell, + StructuredListHead, + StructuredListRow, + StructuredListWrapper +} from 'carbon-components-react'; + +import Header from '../../components/Header'; + +import '../../components/Definitions/Definitions.scss'; + +/* istanbul ignore next */ +const Home = () => { + return ( +
    +
    +
    + + Home + +
    +
    + +
    + + + + Task + + + + + + Pipelines + + + + + Tasks + + + + +
    +
    + ); +}; + +export default Home; diff --git a/src/containers/Home/index.js b/src/containers/Home/index.js new file mode 100644 index 000000000..3b0b8bacb --- /dev/null +++ b/src/containers/Home/index.js @@ -0,0 +1,14 @@ +/* +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. +*/ + +export { default } from './Home'; diff --git a/src/containers/PipelineRun/PipelineRun.js b/src/containers/PipelineRun/PipelineRun.js index 5e5479103..42170afdf 100644 --- a/src/containers/PipelineRun/PipelineRun.js +++ b/src/containers/PipelineRun/PipelineRun.js @@ -17,12 +17,18 @@ import { InlineNotification } from 'carbon-components-react'; import { getPipelineRun, getTaskRun, getTasks } from '../../api'; -import PipelineRunHeader from '../../components/PipelineRunHeader'; +import RunHeader from '../../components/RunHeader'; import StepDetails from '../../components/StepDetails'; import TaskTree from '../../components/TaskTree'; -import { getStatus } from '../../utils'; +import { + getStatus, + taskRunStep, + selectedTask, + selectedTaskRun, + stepsStatus +} from '../../utils'; -import '../../components/PipelineRun/PipelineRun.scss'; +import '../../components/Run/Run.scss'; /* istanbul ignore next */ class PipelineRunContainer extends Component { @@ -93,15 +99,17 @@ class PipelineRunContainer extends Component { const { pipelineRun: { status: { taskRuns: taskRunDetails } - } + }, + tasks } = this.state; taskRuns = taskRuns.map(taskRun => { const taskName = taskRun.spec.taskRef.name; + const task = selectedTask(taskName, tasks); const taskRunName = taskRun.metadata.name; const { reason, status: succeeded } = getStatus(taskRun); const { pipelineTaskName } = taskRunDetails[taskRunName]; - const steps = this.steps(taskRun.status.steps, taskName); + const steps = stepsStatus(task.spec.steps, taskRun.status.steps); return { id: taskRun.metadata.uid, pipelineTaskName, @@ -117,62 +125,6 @@ class PipelineRunContainer extends Component { this.setState({ taskRuns }); } - step() { - const { selectedStepId, selectedTaskId, taskRuns } = this.state; - const taskRun = taskRuns.find(run => run.id === selectedTaskId); - if (!taskRun) { - return {}; - } - - const step = taskRun.steps.find(s => s.id === selectedStepId); - if (!step) { - return {}; - } - - const { id, stepName, stepStatus, status, reason, ...definition } = step; - - return { - definition, - reason, - stepName, - stepStatus, - status, - taskRun - }; - } - - steps(stepsStatus, taskName) { - const { tasks } = this.state; - const task = tasks.find(t => t.metadata.name === taskName); - if (!task) { - return []; - } - - const steps = task.spec.steps.map((step, index) => { - const stepStatus = stepsStatus[index]; - let status; - let reason; - if (stepStatus.terminated) { - status = 'terminated'; - ({ reason } = stepStatus.terminated); - } else if (stepStatus.running) { - status = 'running'; - } else if (stepStatus.waiting) { - status = 'waiting'; - } - - return { - ...step, - reason, - status, - stepStatus, - stepName: step.name, - id: step.name - }; - }); - return steps; - } - render() { const { match } = this.props; const { pipelineName, pipelineRunName } = match.params; @@ -191,14 +143,13 @@ class PipelineRunContainer extends Component { errorMessage = error.response.status === 404 ? 'Not Found' : 'Error'; } - const { - definition, - reason, - status, - stepName, - stepStatus, + const taskRun = selectedTaskRun(selectedTaskId, taskRuns) || {}; + + const { definition, reason, status, stepName, stepStatus } = taskRunStep( + selectedStepId, taskRun - } = this.step(); + ); + const { lastTransitionTime, reason: pipelineRunReason, @@ -206,15 +157,17 @@ class PipelineRunContainer extends Component { } = getStatus(pipelineRun); return ( -
    - +
    {error ? ( diff --git a/src/containers/PipelineRuns/PipelineRuns.js b/src/containers/PipelineRuns/PipelineRuns.js index a6863a549..d489e341b 100644 --- a/src/containers/PipelineRuns/PipelineRuns.js +++ b/src/containers/PipelineRuns/PipelineRuns.js @@ -28,7 +28,7 @@ import { import Header from '../../components/Header'; import { getPipelineRuns } from '../../api'; -import { getPipelineRunStatusIcon, getStatus } from '../../utils'; +import { getStatusIcon, getStatus } from '../../utils'; /* istanbul ignore next */ class PipelineRuns extends Component { @@ -59,9 +59,9 @@ class PipelineRuns extends Component { const { error, loading, pipelineRuns } = this.state; return ( -
    +
    -
    +
    Pipelines @@ -111,7 +111,7 @@ class PipelineRuns extends Component { return ( @@ -126,7 +126,7 @@ class PipelineRuns extends Component { data-reason={reason} data-status={status} > - {getPipelineRunStatusIcon({ reason, status })} + {getStatusIcon({ reason, status })} {pipelineRun.status.conditions[0].message} diff --git a/src/containers/Pipelines/Pipelines.js b/src/containers/Pipelines/Pipelines.js index 38de36d8b..0227fa6a2 100644 --- a/src/containers/Pipelines/Pipelines.js +++ b/src/containers/Pipelines/Pipelines.js @@ -30,7 +30,7 @@ import Header from '../../components/Header'; import { getPipelines } from '../../api'; import { fetchPipelines } from '../../actions/pipeline'; -import '../../components/Pipelines/Pipelines.scss'; +import '../../components/Definitions/Definitions.scss'; /* istanbul ignore next */ export class Pipelines extends Component { @@ -53,9 +53,9 @@ export class Pipelines extends Component { const { error, loading, pipelines } = this.state; return ( -
    +
    -
    +
    Pipelines @@ -90,7 +90,7 @@ export class Pipelines extends Component { const pipelineName = pipeline.metadata.name; return ( diff --git a/src/containers/TaskRuns/TaskRuns.js b/src/containers/TaskRuns/TaskRuns.js new file mode 100644 index 000000000..6c13e72eb --- /dev/null +++ b/src/containers/TaskRuns/TaskRuns.js @@ -0,0 +1,194 @@ +/* +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, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { InlineNotification } from 'carbon-components-react'; + +import { getTasks, getTaskRuns } from '../../api'; + +import RunHeader from '../../components/RunHeader'; +import StepDetails from '../../components/StepDetails'; +import TaskTree from '../../components/TaskTree'; +import { + getStatus, + stepsStatus, + taskRunStep, + selectedTaskRun +} from '../../utils'; + +import '../../components/Run/Run.scss'; + +/* istanbul ignore next */ +class TaskRunsContainer extends Component { + // once redux store is available errors will be handled properly with dedicated components + static notification(notification) { + const { kind, message } = notification; + const titles = { + info: 'Task runs not available', + error: 'Error loading task run' + }; + return ( + + ); + } + + state = { + notification: null, + loading: true, + selectedStepId: null, + selectedTaskId: null, + taskRuns: [] + }; + + async componentDidMount() { + try { + const { match } = this.props; + const { taskName } = match.params; + await this.loadTaskRuns(taskName); + } catch (error) { + let message = error; + if (error.response) { + message = error.response.status === 404 ? 'Not Found' : 'Error'; + } + this.setState({ + notification: { kind: 'error', message }, + loading: false + }); + } + } + + componentDidUpdate(prevProps) { + const { match } = this.props; + const { taskName } = match.params; + if (taskName !== prevProps.match.params.taskName) { + this.loadTaskRuns(taskName); + } + } + + handleTaskSelected = (selectedTaskId, selectedStepId) => { + this.setState({ selectedStepId, selectedTaskId }); + }; + + async loadTaskRuns(selectedTaskName) { + let notification; + const tasks = await getTasks(); + const task = tasks.find( + currentTask => currentTask.metadata.name === selectedTaskName + ); + let taskRuns = await getTaskRuns(); + taskRuns = taskRuns + .filter( + taskRun => + taskRun.spec.taskRef && taskRun.spec.taskRef.name === selectedTaskName + ) + .map(taskRun => { + const taskName = taskRun.spec.taskRef.name; + const taskRunName = taskRun.metadata.name; + const { reason, status: succeeded } = getStatus(taskRun); + const pipelineTaskName = taskRunName; + const runSteps = stepsStatus(task.spec.steps, taskRun.status.steps); + const { startTime } = taskRun.status; + return { + id: taskRun.metadata.uid, + pipelineTaskName, + pod: taskRun.status.podName, + reason, + steps: runSteps, + succeeded, + taskName, + taskRunName, + startTime + }; + }); + + if (taskRuns.length === 0) { + notification = { + kind: 'info', + message: 'Task has never run' + }; + } + + this.setState({ taskRuns, notification, loading: false }); + } + + render() { + const { match } = this.props; + const { taskName } = match.params; + const { + loading, + selectedStepId, + selectedTaskId, + taskRuns, + notification + } = this.state; + + const taskRun = selectedTaskRun(selectedTaskId, taskRuns) || {}; + + const { definition, reason, status, stepName, stepStatus } = taskRunStep( + selectedStepId, + taskRun + ); + + return ( +
    + +
    + {notification ? ( + TaskRunsContainer.notification(notification) + ) : ( +
    + + {selectedStepId && ( + + )} +
    + )} +
    +
    + ); + } +} + +TaskRunsContainer.propTypes = { + match: PropTypes.shape({ + params: PropTypes.shape({ + taskName: PropTypes.string.isRequired + }).isRequired + }).isRequired +}; + +export default TaskRunsContainer; diff --git a/src/containers/TaskRuns/TaskRuns.test.js b/src/containers/TaskRuns/TaskRuns.test.js new file mode 100644 index 000000000..5c204e174 --- /dev/null +++ b/src/containers/TaskRuns/TaskRuns.test.js @@ -0,0 +1,76 @@ +/* +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 { waitForElement } from 'react-testing-library'; + +import TaskRunsContainer from './TaskRuns'; +import * as API from '../../api'; +import { renderWithRouter } from '../../utils/test'; + +beforeEach(jest.resetAllMocks); + +it('TaskRunsContainer renders', async () => { + const taskName = 'taskName'; + const match = { + params: { + taskName + } + }; + const tasksCall = jest.spyOn(API, 'getTasks').mockImplementation(() => ''); + const taskRunsCall = jest + .spyOn(API, 'getTaskRuns') + .mockImplementation(() => ''); + const { getByText } = renderWithRouter(); + await waitForElement(() => getByText(taskName)); + expect(tasksCall).toHaveBeenCalledTimes(1); + expect(taskRunsCall).toHaveBeenCalledTimes(0); +}); + +it('TaskRunsContainer handles info state', async () => { + const notificationMessage = 'Task runs not available'; + const taskName = 'taskName'; + const match = { + params: { + taskName + } + }; + const tasksCall = jest + .spyOn(API, 'getTasks') + .mockImplementation(() => [{ metadata: { name: taskName } }]); + const taskRunsCall = jest + .spyOn(API, 'getTaskRuns') + .mockImplementation(() => []); + const { getByText } = renderWithRouter(); + await waitForElement(() => getByText(notificationMessage)); + expect(tasksCall).toHaveBeenCalledTimes(1); + expect(taskRunsCall).toHaveBeenCalledTimes(1); +}); + +it('TaskRunsContainer handles error state', async () => { + const match = { + params: { + taskName: 'foo' + } + }; + const getTasks = jest.spyOn(API, 'getTasks').mockImplementation(() => { + const error = new Error(); + error.response = { + status: 504 + }; + throw error; + }); + const { getByText } = renderWithRouter(); + await waitForElement(() => getByText('Error loading task run')); + expect(getTasks).toHaveBeenCalledTimes(1); +}); diff --git a/src/containers/TaskRuns/index.js b/src/containers/TaskRuns/index.js new file mode 100644 index 000000000..5af5b6dd8 --- /dev/null +++ b/src/containers/TaskRuns/index.js @@ -0,0 +1,14 @@ +/* +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. +*/ + +export { default } from './TaskRuns'; diff --git a/src/containers/Tasks/Tasks.js b/src/containers/Tasks/Tasks.js new file mode 100644 index 000000000..3d7b7343f --- /dev/null +++ b/src/containers/Tasks/Tasks.js @@ -0,0 +1,109 @@ +/* +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, { Component } from 'react'; +import { Link } from 'react-router-dom'; +import { + Breadcrumb, + BreadcrumbItem, + InlineNotification, + StructuredListBody, + StructuredListCell, + StructuredListHead, + StructuredListRow, + StructuredListSkeleton, + StructuredListWrapper +} from 'carbon-components-react'; + +import Header from '../../components/Header'; +import { getTasks } from '../../api'; + +import '../../components/Definitions/Definitions.scss'; + +/* istanbul ignore next */ +class Tasks extends Component { + state = { + error: null, + loading: true, + tasks: [] + }; + + async componentDidMount() { + try { + const tasks = await getTasks(); + this.setState({ tasks, loading: false }); + } catch (error) { + this.setState({ error, loading: false }); + } + } + + render() { + const { error, loading, tasks } = this.state; + + return ( +
    +
    +
    + + Tasks + +
    +
    +
    + {(() => { + if (loading) { + return ; + } + + if (error) { + return ( + + ); + } + + return ( + + + + Task + + + + {tasks.map(task => { + const taskName = task.metadata.name; + return ( + + + {taskName} + + + ); + })} + + + ); + })()} +
    +
    + ); + } +} + +export default Tasks; diff --git a/src/containers/Tasks/index.js b/src/containers/Tasks/index.js new file mode 100644 index 000000000..5fc7615bd --- /dev/null +++ b/src/containers/Tasks/index.js @@ -0,0 +1,14 @@ +/* +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. +*/ + +export { default } from './Tasks'; diff --git a/src/containers/index.js b/src/containers/index.js index 025cce820..19b57a5c6 100644 --- a/src/containers/index.js +++ b/src/containers/index.js @@ -14,3 +14,6 @@ limitations under the License. export { default as PipelineRun } from './PipelineRun'; export { default as PipelineRuns } from './PipelineRuns'; export { default as Pipelines } from './Pipelines'; +export { default as Tasks } from './Tasks'; +export { default as TaskRuns } from './TaskRuns'; +export { default as Home } from './Home'; diff --git a/src/utils/index.js b/src/utils/index.js index 52e31b9c1..f43544c9c 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -21,7 +21,7 @@ export function getStatus(resource) { return conditions.find(condition => condition.type === 'Succeeded') || {}; } -export function getPipelineRunStatusIcon({ reason, status }) { +export function getStatusIcon({ reason, status }) { if (status === 'Unknown' && reason === 'Running') { return ; } @@ -35,3 +35,57 @@ export function getPipelineRunStatusIcon({ reason, status }) { return icon ? : null; } + +export function taskRunStep(selectedStepId, taskRun) { + if (!taskRun || !taskRun.steps) { + return {}; + } + const step = taskRun.steps.find(s => s.id === selectedStepId); + if (!step) { + return {}; + } + + const { id, stepName, stepStatus, status, reason, ...definition } = step; + + return { + definition, + reason, + stepName, + stepStatus, + status + }; +} + +export function selectedTask(selectedTaskName, tasks) { + return tasks.find(t => t.metadata.name === selectedTaskName); +} + +export function selectedTaskRun(selectedTaskId, taskRuns) { + return taskRuns.find(run => run.id === selectedTaskId); +} + +export function stepsStatus(taskSteps, taskRunStepsStatus) { + const steps = taskSteps.map((step, index) => { + const stepStatus = taskRunStepsStatus ? taskRunStepsStatus[index] : {}; + let status; + let reason; + if (stepStatus.terminated) { + status = 'terminated'; + ({ reason } = stepStatus.terminated); + } else if (stepStatus.running) { + status = 'running'; + } else if (stepStatus.waiting) { + status = 'waiting'; + } + + return { + ...step, + reason, + status, + stepStatus, + stepName: step.name, + id: step.name + }; + }); + return steps; +} diff --git a/src/utils/index.test.js b/src/utils/index.test.js index 5cd400390..5a10e94c1 100644 --- a/src/utils/index.test.js +++ b/src/utils/index.test.js @@ -11,7 +11,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { getStatus } from '.'; +import { getStatus, taskRunStep, selectedTask, stepsStatus } from '.'; it('getStatus', () => { const taskRun = { @@ -42,3 +42,138 @@ it('getStatus with no status', () => { const status = getStatus(taskRun); expect(status).toEqual({}); }); + +it('taskRunSteps with no taskRun', () => { + const taskRun = null; + const step = taskRunStep('selected run', taskRun); + expect(step).toEqual({}); +}); + +it('taskRunStep with no taskRun', () => { + const taskRun = null; + const step = taskRunStep('selected run', taskRun); + expect(step).toEqual({}); +}); + +it('taskRunStep with no steps', () => { + const taskRun = {}; + const step = taskRunStep('selected run', taskRun); + expect(step).toEqual({}); +}); + +it('taskRunStep with no steps', () => { + const stepName = 'testName'; + const id = 'id'; + const targetStep = { id, stepName }; + const taskRun = { steps: [targetStep] }; + const step = taskRunStep(id, taskRun); + expect(step.stepName).toEqual(stepName); +}); + +it('taskRunStep does not contain selected step', () => { + const stepName = 'testName'; + const id = 'id'; + const targetStep = { id, stepName }; + const taskRun = { steps: [targetStep] }; + const step = taskRunStep('wrong id', taskRun); + expect(step).toEqual({}); +}); + +it('taskRunStep with step finds step', () => { + const stepName = 'testName'; + const id = 'id'; + const targetStep = { id, stepName }; + const taskRun = { steps: [targetStep] }; + const step = taskRunStep(id, taskRun); + expect(step.stepName).toEqual(stepName); +}); + +it('selectedTask find not exists', () => { + const taskName = 'testName'; + const foundTask = selectedTask(taskName, []); + expect(foundTask).toEqual(undefined); +}); + +it('selectedTask find exists', () => { + const taskName = 'testName'; + const expectedTask = { metadata: { name: taskName } }; + const foundTask = selectedTask(taskName, [expectedTask]); + expect(foundTask.metadata.name).toEqual(taskName); +}); + +it('stepsStatus no steps', () => { + const taskSteps = []; + const taskRunStepsStatus = []; + const steps = stepsStatus(taskSteps, taskRunStepsStatus); + expect(steps).toEqual([]); +}); + +it('stepsStatus no status', () => { + const taskSteps = []; + const taskRunStepsStatus = undefined; + const steps = stepsStatus(taskSteps, taskRunStepsStatus); + expect(steps).toEqual([]); +}); + +it('stepsStatus step is running', () => { + const stepName = 'testStep'; + const taskSteps = [{ name: stepName, image: 'test' }]; + const taskRunStepsStatus = [{ running: { startedAt: '2019' } }]; + const steps = stepsStatus(taskSteps, taskRunStepsStatus); + const returnedStep = steps[0]; + expect(returnedStep.status).toEqual('running'); + expect(returnedStep.stepName).toEqual(stepName); +}); + +it('stepsStatus step is completed', () => { + const reason = 'completed'; + const stepName = 'testStep'; + const taskSteps = [{ name: stepName, image: 'test' }]; + const taskRunStepsStatus = [ + { + terminated: { + exitCode: 0, + reason, + startedAt: '2019', + finishedAt: '2019', + containerID: 'containerd://testid' + } + } + ]; + const steps = stepsStatus(taskSteps, taskRunStepsStatus); + const returnedStep = steps[0]; + expect(returnedStep.status).toEqual('terminated'); + expect(returnedStep.stepName).toEqual(stepName); + expect(returnedStep.reason).toEqual(reason); +}); + +it('stepsStatus step is terminated with error', () => { + const reason = 'Error'; + const stepName = 'testStep'; + const taskSteps = [{ name: stepName, image: 'test' }]; + const taskRunStepsStatus = [ + { + terminated: { + exitCode: 1, + reason, + startedAt: '2019', + finishedAt: '2019', + containerID: 'containerd://testid' + } + } + ]; + const steps = stepsStatus(taskSteps, taskRunStepsStatus); + const returnedStep = steps[0]; + expect(returnedStep.status).toEqual('terminated'); + expect(returnedStep.stepName).toEqual(stepName); + expect(returnedStep.reason).toEqual(reason); +}); + +it('stepsStatus step is waiting', () => { + const stepName = 'testStep'; + const taskSteps = [{ name: stepName, image: 'test' }]; + const taskRunStepsStatus = [{ waiting: {} }]; + const steps = stepsStatus(taskSteps, taskRunStepsStatus); + const returnedStep = steps[0]; + expect(returnedStep.status).toEqual('waiting'); +});