├── .gitignore
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.css
├── index.html
└── manifest.json
├── screenshot1.png
├── screenshot2.png
├── src
├── App.tsx
├── ReduxRoot.tsx
├── actions
│ ├── dashboard.ts
│ └── index.ts
├── components
│ ├── PipelineState.tsx
│ ├── PipelineTable.tsx
│ └── index.ts
├── configureStore.ts
├── index.tsx
├── model
│ └── model.ts
├── pages
│ ├── DashboardPage.tsx
│ └── LoginPage.tsx
├── reducers
│ ├── createReducer.ts
│ ├── dashboard.ts
│ └── index.ts
└── withRoot.tsx
├── tsconfig.json
├── tsconfig.test.json
├── tslint.json
└── typings
└── misc.d.ts
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 innFactory
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # A simple dashboard for monitoring your aws codepipelines
2 |
3 |
4 |
5 |
6 | ## How to use
7 |
8 | Download or clone this repo
9 |
10 | ```bash
11 | git clone https://github.com/innFactory/aws-codepipeline-dashboard
12 | cd create-react-app-material-typescript-redux
13 | ```
14 |
15 | Install it and run:
16 |
17 | ```bash
18 | npm i
19 | npm start
20 | ```
21 |
22 | Got to http://localhost:3000/
23 |
24 | ## Todo List
25 | - [x] Show states of codepipelines
26 | - [x] Refresh codepipelines every 8-20 seconds
27 | - [ ] Add pretty animations for each state
28 | - [ ] Support for custom grid sizes
29 | - [ ] Add theming
30 |
31 | ## Contributors
32 |
33 | * [Anton Spöck](https://github.com/spoeck)
34 |
35 | Powered by [innFactory](https://innfactory.de/)
36 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "create-react-app-material-typescript-redux",
3 | "version": "0.0.1",
4 | "private": true,
5 | "dependencies": {
6 | "aws-sdk": "^2.233.1",
7 | "localforage": "^1.7.1",
8 | "material-ui": "1.0.0-beta.44",
9 | "material-ui-icons": "^1.0.0-beta.36",
10 | "npm": "^5.7.1",
11 | "react": "16.2.0",
12 | "react-dom": "16.3.2",
13 | "react-grid-layout": "^0.16.6",
14 | "react-redux": "^5.0.7",
15 | "react-router": "^4.2.0",
16 | "react-scripts-ts": "latest",
17 | "redux": "^3.7.2",
18 | "redux-logger": "^3.0.6",
19 | "redux-persist": "^5.9.1",
20 | "redux-thunk": "^2.2.0"
21 | },
22 | "devDependencies": {
23 | "@types/history": "^4.6.2",
24 | "@types/jest": "22.1.0",
25 | "@types/node": "^9.4.0",
26 | "@types/react": "^16.3.2",
27 | "@types/react-dom": "^16.0.5",
28 | "@types/react-grid-layout": "^0.16.4",
29 | "@types/react-redux": "^5.0.19",
30 | "@types/react-router": "^4.0.18",
31 | "@types/redux-logger": "^3.0.5",
32 | "@types/webpack-env": "^1.13.5",
33 | "csstype": "^2.4.2",
34 | "redux-devtools-extension": "^2.13.2",
35 | "typescript": "2.8.3"
36 | },
37 | "scripts": {
38 | "start": "react-scripts-ts start",
39 | "build": "react-scripts-ts build",
40 | "test": "react-scripts-ts test --env=jsdom",
41 | "eject": "react-scripts-ts eject"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/innFactory/aws-codepipeline-dashboard/29fa8731eaada9f0db1348f83ee925ad0a04f7ca/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.css:
--------------------------------------------------------------------------------
1 | html, body, #app, #root, #app>div {
2 | height: 100% !important;
3 | }
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
15 |
24 |
25 | My page
26 |
27 |
28 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/screenshot1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/innFactory/aws-codepipeline-dashboard/29fa8731eaada9f0db1348f83ee925ad0a04f7ca/screenshot1.png
--------------------------------------------------------------------------------
/screenshot2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/innFactory/aws-codepipeline-dashboard/29fa8731eaada9f0db1348f83ee925ad0a04f7ca/screenshot2.png
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import withStyles, { WithStyles, StyleRulesCallback } from 'material-ui/styles/withStyles';
3 | import withRoot from './withRoot';
4 | import LoginPage from './pages/LoginPage';
5 | import DashboardPage from './pages/DashboardPage';
6 | import { Router, Route, RouteComponentProps } from 'react-router';
7 | import { createBrowserHistory } from 'history';
8 | import { RootState } from './reducers/index';
9 | import { connect } from 'react-redux';
10 |
11 | export namespace App {
12 | export interface Props extends RouteComponentProps {
13 | }
14 |
15 | export interface State {
16 | mobileOpen: boolean;
17 | }
18 | }
19 |
20 | const history = createBrowserHistory();
21 |
22 | class App extends React.Component {
23 |
24 | state = {
25 | mobileOpen: true,
26 | };
27 |
28 | routes = (
29 |
30 |
31 |
32 |
33 |
34 | );
35 |
36 | render() {
37 |
38 | return (
39 |
40 |
41 |
42 | {this.routes}
43 |
44 |
45 |
46 | );
47 | }
48 | }
49 |
50 | const styles: StyleRulesCallback = theme => ({
51 | root: {
52 | width: '100%',
53 | height: '100%',
54 | zIndex: 1,
55 | overflow: 'hidden',
56 | },
57 | appFrame: {
58 | position: 'relative',
59 | display: 'flex',
60 | width: '100%',
61 | height: '100%',
62 | },
63 | appBar: {
64 | zIndex: theme.zIndex.drawer + 1,
65 | position: 'absolute',
66 | },
67 | content: {
68 | backgroundColor: theme.palette.background.default,
69 | width: '100%',
70 | },
71 | });
72 |
73 | function mapStateToProps(state: RootState) {
74 | return {
75 | };
76 | }
77 |
78 | export default (withRoot(withStyles(styles)<{}>(connect(mapStateToProps)(App))));
79 |
--------------------------------------------------------------------------------
/src/ReduxRoot.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import App from './App';
3 | import {
4 | applyMiddleware,
5 | } from 'redux';
6 | import thunk from 'redux-thunk';
7 | import { createLogger } from 'redux-logger';
8 | import { Provider } from 'react-redux';
9 | import { composeWithDevTools } from 'redux-devtools-extension';
10 | import { PersistGate } from 'redux-persist/integration/react';
11 | import configureStore from './configureStore';
12 |
13 | const logger = (createLogger as any)();
14 |
15 | var middleware = applyMiddleware(logger, thunk);
16 |
17 | if (process.env.NODE_ENV === 'development') {
18 | middleware = composeWithDevTools(middleware);
19 | }
20 |
21 | const { persistor, store } = configureStore();
22 |
23 | class ReduxRoot extends React.Component {
24 |
25 | state = {
26 | mobileOpen: true,
27 | };
28 |
29 | render() {
30 |
31 | return (
32 |
33 | Loading...} persistor={persistor}>
34 |
35 |
36 |
37 | );
38 | }
39 | }
40 |
41 | export default ReduxRoot;
42 |
--------------------------------------------------------------------------------
/src/actions/dashboard.ts:
--------------------------------------------------------------------------------
1 | import { Action, ActionType, Credentials, PipelineState } from '../model/model';
2 | const AWS = require('aws-sdk');
3 | let codepipeline = new AWS.CodePipeline();
4 |
5 | export function setCredentials(credentials: Credentials): Action {
6 |
7 | return {
8 | type: ActionType.SET_CREDENTIALS,
9 | payload: credentials
10 | };
11 | }
12 |
13 | export function setPipelinesToObserve(pipelines: any[]): Action {
14 |
15 | return {
16 | type: ActionType.SET_PIPELINES_TO_OBSERVE,
17 | payload: pipelines
18 | };
19 | }
20 |
21 | export function authenticate() {
22 |
23 | return (dispatch: Function, getState: Function) => auth(getState);
24 | }
25 |
26 | function auth(getState: Function) {
27 | console.log('auth');
28 | AWS.config = new AWS.Config();
29 | AWS.config.region = getState().credentials.region;
30 | AWS.config.update(getState().credentials);
31 | codepipeline = new AWS.CodePipeline();
32 | }
33 |
34 | export function fetchAllPipelines() {
35 | return (dispatch: Function, getState: Function) => {
36 | codepipeline.listPipelines({}, (e: any, d: any) => {
37 | if (e) {
38 | console.log(e);
39 | setTimeout(() => auth(getState), 3000);
40 | setTimeout(() => fetchAllPipelines(), 4000);
41 |
42 | } else {
43 | dispatch({ type: ActionType.SET_ALL_PIPELINES, payload: d.pipelines });
44 | }
45 | });
46 | };
47 | }
48 |
49 | export function reloadPipelineState(pipelineName: string, callback: Function) {
50 |
51 | return (dispatch: Function, getState: Function) => {
52 | codepipeline.getPipelineState({ name: pipelineName }, (e: any, d: any) => {
53 | if (e) {
54 | console.log(e);
55 | setTimeout(() => auth(getState), 3000);
56 | } else {
57 | console.log(d);
58 |
59 | let failed = false;
60 | let inProgress = false;
61 | d.stageStates.forEach((state: any) => {
62 | if (!failed && !inProgress) {
63 | const pipelineState: PipelineState = {
64 | pipelineName: d.pipelineName,
65 | state: state.latestExecution.status,
66 | stageName: state.stageName,
67 | };
68 |
69 | callback(pipelineState);
70 | }
71 | if (state.latestExecution.status === 'Failed') {
72 | failed = true;
73 | } else if (state.latestExecution.status === 'InProgress') {
74 | inProgress = true;
75 | }
76 | });
77 | }
78 | });
79 | };
80 | }
--------------------------------------------------------------------------------
/src/actions/index.ts:
--------------------------------------------------------------------------------
1 | import * as DashboardActions from './dashboard';
2 |
3 | export const ActionCreators = Object.assign({}, DashboardActions);
4 |
--------------------------------------------------------------------------------
/src/components/PipelineState.tsx:
--------------------------------------------------------------------------------
1 | import { Paper, Typography } from 'material-ui';
2 | import { LinearProgress } from 'material-ui/Progress';
3 | import withStyles, { StyleRulesCallback, WithStyles } from 'material-ui/styles/withStyles';
4 | import * as React from 'react';
5 | import * as DashboardActions from '../actions/dashboard';
6 | import { PipelineState as PS } from '../model/model';
7 |
8 | export namespace PipelineState {
9 | export interface Props {
10 | dashboardActions: typeof DashboardActions;
11 | pipeline: any;
12 | }
13 |
14 | export interface State {
15 | isLoading: boolean;
16 | pipelineState: PS;
17 | }
18 | }
19 | const defaultState: PS = { pipelineName: '', state: 'UNKNOWN', stageName: '' };
20 | class PipelineState extends React.Component {
21 |
22 | state = {
23 | isLoading: true,
24 | pipelineState: defaultState
25 | };
26 |
27 | componentDidMount() {
28 | const { dashboardActions, pipeline } = this.props;
29 | const interval = getRndInteger(8, 20) * 1000;
30 |
31 | dashboardActions.reloadPipelineState(pipeline.name, (d: PS) => {
32 | this.setState({ pipelineState: d, isLoading: false });
33 | });
34 | setInterval(() =>
35 | this.setState({ isLoading: true }, () => dashboardActions.reloadPipelineState(pipeline.name, (d: PS) => {
36 | this.setState({ pipelineState: d, isLoading: false });
37 | // tslint:disable-next-line:align
38 | })), interval);
39 | }
40 |
41 | render() {
42 | const { classes } = this.props;
43 | const { isLoading } = this.state;
44 | return (
45 |
46 |
47 | {isLoading && }
48 |
49 |
50 | {this.renderName()}
51 | {this.renderStage()}
52 | {this.renderState()}
53 |
54 |
55 | );
56 | }
57 |
58 | renderName() {
59 | const { pipelineState } = this.state;
60 |
61 | const name = pipelineState ? pipelineState.pipelineName : '';
62 |
63 | if (name) {
64 | return ({name});
65 | } else { return null; }
66 | }
67 |
68 | renderStage() {
69 | const { pipelineState } = this.state;
70 |
71 | const name = pipelineState ? pipelineState.stageName : '';
72 |
73 | if (name) {
74 | return (
75 |
79 | {name === 'UNKNOWN' ? 'Loading' : name}
80 | );
81 | } else { return null; }
82 | }
83 |
84 | renderState() {
85 | const { pipelineState } = this.state;
86 |
87 | const state = pipelineState ? pipelineState.state : '';
88 |
89 | if (state) {
90 | return (
91 |
95 | {state === 'UNKNOWN' ? 'Loading ...' : state}
96 | );
97 | } else { return null; }
98 | }
99 |
100 | getBackgroundColor() {
101 | switch (this.state.pipelineState.state) {
102 | case 'Failed': return '#fc9faa'; // #f7cdd2
103 | case 'InProgress': return '#a6c1ed';
104 | case 'Succeeded': return '#b5f2b9';
105 | default: return null;
106 | }
107 | }
108 |
109 | }
110 |
111 | function getRndInteger(min: number, max: number) {
112 | return Math.floor(Math.random() * (max - min)) + min;
113 | }
114 |
115 | const styles: StyleRulesCallback = theme => ({
116 | container: {
117 | width: '100%',
118 | height: '100%',
119 | },
120 | progressBarWrapper: {
121 | height: 10
122 | },
123 | textWrapper: {
124 | display: 'flex',
125 | flexDirection: 'column',
126 | height: '100%',
127 | padding: 10,
128 | paddingBottom: 20
129 | },
130 | stage: {
131 | flexGrow: 10,
132 | paddingBottom: 10,
133 | },
134 | state: {
135 | paddingBottom: 0
136 | }
137 | });
138 |
139 | export default withStyles(styles)(PipelineState);
140 |
--------------------------------------------------------------------------------
/src/components/PipelineTable.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import Table, { TableBody, TableCell, TableHead, TableRow } from 'material-ui/Table';
3 | import withStyles, { WithStyles, StyleRulesCallback } from 'material-ui/styles/withStyles';
4 | import Checkbox from 'material-ui/Checkbox/Checkbox';
5 | import * as DashboardActions from '../actions/dashboard';
6 |
7 | export namespace PipelineTable {
8 | export interface Props {
9 | allPipelines: any[];
10 | pipelinesToObserve: any[];
11 | dashboardActions: typeof DashboardActions;
12 | }
13 | }
14 |
15 | class PipelineTable extends React.Component {
16 |
17 | constructor(props?: (WithStyles & PipelineTable.Props), context?: any) {
18 | super(props as any, context);
19 | }
20 |
21 | onRowClick(pipeline: any) {
22 | const { dashboardActions, pipelinesToObserve } = this.props;
23 |
24 | const index = pipelinesToObserve.findIndex(p => p.name === pipeline.name);
25 |
26 | console.log(index);
27 |
28 | if (index < 0) {
29 | dashboardActions.setPipelinesToObserve([...pipelinesToObserve, pipeline]);
30 | } else {
31 | let tempArray = [...pipelinesToObserve];
32 | tempArray.splice(index, 1);
33 | dashboardActions.setPipelinesToObserve(tempArray);
34 | }
35 | }
36 |
37 | render() {
38 | const { classes, pipelinesToObserve, allPipelines } = this.props;
39 |
40 | return (
41 |
42 |
43 |
44 | Display
45 | Pipeline
46 |
47 |
48 |
49 | {allPipelines.map(p => {
50 | return (
51 | this.onRowClick(p)}
55 | >
56 |
57 | pi.name === p.name) >= 0}
59 | />
60 |
61 | {p.name}
62 |
63 | );
64 | })}
65 |
66 |
67 | );
68 | }
69 | }
70 |
71 | const styles: StyleRulesCallback = theme => ({
72 | table: {
73 | width: '100%',
74 | },
75 | });
76 |
77 | export default withStyles(styles)(PipelineTable);
--------------------------------------------------------------------------------
/src/components/index.ts:
--------------------------------------------------------------------------------
1 | import PipelineTable from './PipelineTable';
2 |
3 | export {
4 | PipelineTable
5 | };
6 |
7 | export default PipelineTable;
--------------------------------------------------------------------------------
/src/configureStore.ts:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware } from 'redux';
2 | import { persistStore, persistReducer } from 'redux-persist';
3 | import * as localforage from 'localforage';
4 | import rootReducer from './reducers';
5 | import thunk from 'redux-thunk';
6 | import { createLogger } from 'redux-logger';
7 | import { composeWithDevTools } from 'redux-devtools-extension';
8 |
9 | const persistConfig = {
10 | key: 'root',
11 | storage: localforage,
12 | };
13 |
14 | const logger = (createLogger as any)();
15 |
16 | var middleware = applyMiddleware(logger, thunk);
17 |
18 | if (process.env.NODE_ENV === 'development') {
19 | middleware = composeWithDevTools(middleware);
20 | }
21 |
22 | const persistedReducer = persistReducer(persistConfig, rootReducer);
23 |
24 | export default () => {
25 | let store = createStore(persistedReducer, {}, middleware);
26 | let persistor = persistStore(store);
27 | return { store, persistor };
28 | };
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as ReactDOM from 'react-dom';
3 | import ReduxRoot from './ReduxRoot';
4 |
5 | const rootEl = document.getElementById('root');
6 | ReactDOM.render(, rootEl);
7 |
8 | if (module.hot) {
9 | module.hot.accept('./ReduxRoot', () => {
10 | const NextApp = require('./ReduxRoot').default;
11 | ReactDOM.render(
12 | ,
13 | rootEl
14 | );
15 | });
16 | }
--------------------------------------------------------------------------------
/src/model/model.ts:
--------------------------------------------------------------------------------
1 |
2 | export interface Credentials {
3 | accessKeyId: string;
4 | secretAccessKey: string;
5 | region: string;
6 | }
7 |
8 | export enum ActionType {
9 | SET_CREDENTIALS,
10 | SET_PIPELINES_TO_OBSERVE,
11 | AUTHENTICATE,
12 | SET_ALL_PIPELINES
13 | }
14 |
15 | export interface Action {
16 | type: ActionType;
17 | payload: T;
18 | }
19 |
20 | export interface PipelineState {
21 | pipelineName: string;
22 | stageName: string | 'ALL';
23 | state: 'InProgress' | 'Succeeded' | 'Failed' | 'UNKNOWN';
24 | }
25 |
--------------------------------------------------------------------------------
/src/pages/DashboardPage.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from 'material-ui';
2 | import SettingsIcon from 'material-ui-icons/Settings';
3 | import Dialog, { DialogActions, DialogContent, DialogTitle } from 'material-ui/Dialog';
4 | import withStyles, { StyleRulesCallback, WithStyles } from 'material-ui/styles/withStyles';
5 | import * as React from 'react';
6 | import { Responsive as ResponsiveGridLayout } from 'react-grid-layout';
7 | import { connect } from 'react-redux';
8 | import { RouteComponentProps } from 'react-router';
9 | import { bindActionCreators } from 'redux';
10 | import * as DashboardActions from '../actions/dashboard';
11 | import { PipelineTable } from '../components';
12 | import PipelineState from '../components/PipelineState';
13 | import { RootState } from '../reducers/index';
14 |
15 | export namespace DashboardPage {
16 | export interface Props extends RouteComponentProps {
17 | pipelinesToObserve: any[];
18 | allPipelines: any[];
19 | dashboardActions: typeof DashboardActions;
20 | }
21 |
22 | export interface State {
23 | showDialog: boolean;
24 | layout: Array;
25 | }
26 | }
27 |
28 | class DashboardPage extends React.Component {
29 | state = {
30 | showDialog: false,
31 | height: 150,
32 | layout: new Array