crono_trigger 0.3.2 → 0.3.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -2
  3. data/README.md +40 -0
  4. data/Rakefile +17 -0
  5. data/crono_trigger.gemspec +4 -1
  6. data/exe/crono_trigger-web +33 -0
  7. data/lib/crono_trigger.rb +20 -2
  8. data/lib/crono_trigger/cli.rb +8 -3
  9. data/lib/crono_trigger/models/signal.rb +52 -0
  10. data/lib/crono_trigger/models/worker.rb +16 -0
  11. data/lib/crono_trigger/polling_thread.rb +58 -20
  12. data/lib/crono_trigger/railtie.rb +15 -0
  13. data/lib/crono_trigger/schedulable.rb +69 -17
  14. data/lib/crono_trigger/version.rb +1 -1
  15. data/lib/crono_trigger/web.rb +163 -0
  16. data/lib/crono_trigger/worker.rb +118 -8
  17. data/lib/generators/crono_trigger/install/install_generator.rb +16 -0
  18. data/lib/generators/crono_trigger/install/templates/install.rb +23 -0
  19. data/lib/generators/crono_trigger/migration/templates/create_table_migration.rb +1 -0
  20. data/lib/generators/crono_trigger/migration/templates/migration.rb +1 -0
  21. data/web/app/.gitignore +21 -0
  22. data/web/app/README.md +2448 -0
  23. data/web/app/images.d.ts +3 -0
  24. data/web/app/package-lock.json +12439 -0
  25. data/web/app/package.json +36 -0
  26. data/web/app/public/favicon.ico +0 -0
  27. data/web/app/public/index.html +45 -0
  28. data/web/app/public/manifest.json +8 -0
  29. data/web/app/src/App.css +5 -0
  30. data/web/app/src/App.test.tsx +9 -0
  31. data/web/app/src/App.tsx +91 -0
  32. data/web/app/src/Models.tsx +61 -0
  33. data/web/app/src/SchedulableRecord.tsx +208 -0
  34. data/web/app/src/SchedulableRecordTableCell.tsx +19 -0
  35. data/web/app/src/SchedulableRecords.tsx +110 -0
  36. data/web/app/src/Signal.tsx +21 -0
  37. data/web/app/src/Signals.tsx +74 -0
  38. data/web/app/src/Worker.tsx +106 -0
  39. data/web/app/src/Workers.tsx +78 -0
  40. data/web/app/src/index.css +5 -0
  41. data/web/app/src/index.tsx +15 -0
  42. data/web/app/src/interfaces.ts +77 -0
  43. data/web/app/tsconfig.json +30 -0
  44. data/web/app/tsconfig.prod.json +3 -0
  45. data/web/app/tsconfig.test.json +6 -0
  46. data/web/app/tslint.json +13 -0
  47. data/web/public/asset-manifest.json +6 -0
  48. data/web/public/favicon.ico +0 -0
  49. data/web/public/manifest.json +8 -0
  50. data/web/public/service-worker.js +1 -0
  51. data/web/public/static/css/main.0f826673.css +2 -0
  52. data/web/public/static/css/main.0f826673.css.map +1 -0
  53. data/web/public/static/js/main.1413dc51.js +2 -0
  54. data/web/public/static/js/main.1413dc51.js.map +1 -0
  55. data/web/views/index.erb +1 -0
  56. data/web/views/signals.erb +9 -0
  57. data/web/views/workers.erb +9 -0
  58. metadata +89 -3
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "app",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "proxy": "http://localhost:9292",
6
+ "dependencies": {
7
+ "@material-ui/core": "^1.4.3",
8
+ "@material-ui/icons": "^2.0.1",
9
+ "classnames": "^2.2.6",
10
+ "date-fns": "^1.29.0",
11
+ "react": "^16.4.2",
12
+ "react-dom": "^16.4.2",
13
+ "react-router-dom": "^4.3.1",
14
+ "react-scripts-ts": "2.17.0",
15
+ "react-syntax-highlighter": "^8.0.1"
16
+ },
17
+ "scripts": {
18
+ "start": "react-scripts-ts start",
19
+ "build": "react-scripts-ts build",
20
+ "test": "react-scripts-ts test --env=jsdom",
21
+ "eject": "react-scripts-ts eject"
22
+ },
23
+ "devDependencies": {
24
+ "@types/classnames": "^2.2.6",
25
+ "@types/date-fns": "^2.6.0",
26
+ "@types/jest": "^23.3.1",
27
+ "@types/lodash": "^4.14.116",
28
+ "@types/node": "^10.5.7",
29
+ "@types/react": "^16.4.8",
30
+ "@types/react-dom": "^16.0.7",
31
+ "@types/react-router-dom": "^4.3.0",
32
+ "@types/react-syntax-highlighter": "0.0.6",
33
+ "lodash": "^4.17.10",
34
+ "typescript": "^3.0.1"
35
+ }
36
+ }
@@ -0,0 +1,45 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
6
+ <meta name="theme-color" content="#000000">
7
+ <!--
8
+ manifest.json provides metadata used when your web app is added to the
9
+ homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
10
+ -->
11
+ <link rel="manifest" href="%PUBLIC_URL%/manifest.json">
12
+ <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
13
+ <!--
14
+ Notice the use of %PUBLIC_URL% in the tags above.
15
+ It will be replaced with the URL of the `public` folder during the build.
16
+ Only files inside the `public` folder can be referenced from the HTML.
17
+
18
+ Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
19
+ work correctly both with client-side routing and a non-root public URL.
20
+ Learn how to configure a non-root public URL by running `npm run build`.
21
+ -->
22
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500">
23
+ <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
24
+ <script>
25
+ window.mountPath = "%PUBLIC_URL%";
26
+ </script>
27
+ <title>CronoTrigger</title>
28
+ </head>
29
+ <body>
30
+ <noscript>
31
+ You need to enable JavaScript to run this app.
32
+ </noscript>
33
+ <div id="root"></div>
34
+ <!--
35
+ This HTML file is a template.
36
+ If you open it directly in the browser, you will see an empty page.
37
+
38
+ You can add webfonts, meta tags, or analytics to this file.
39
+ The build step will place the bundled scripts into the <body> tag.
40
+
41
+ To begin the development, run `npm start` or `yarn start`.
42
+ To create a production bundle, use `npm run build` or `yarn build`.
43
+ -->
44
+ </body>
45
+ </html>
@@ -0,0 +1,8 @@
1
+ {
2
+ "short_name": "CronoTrigger",
3
+ "name": "CronoTrigger Admin",
4
+ "start_url": "./",
5
+ "display": "standalone",
6
+ "theme_color": "#000000",
7
+ "background_color": "#ffffff"
8
+ }
@@ -0,0 +1,5 @@
1
+ .content {
2
+ display: flex;
3
+ justify-content: center;
4
+ align-items: center;
5
+ }
@@ -0,0 +1,9 @@
1
+ import * as React from 'react';
2
+ import * as ReactDOM from 'react-dom';
3
+ import App from './App';
4
+
5
+ it('renders without crashing', () => {
6
+ const div = document.createElement('div');
7
+ ReactDOM.render(<App />, div);
8
+ ReactDOM.unmountComponentAtNode(div);
9
+ });
@@ -0,0 +1,91 @@
1
+ import AppBar from '@material-ui/core/AppBar';
2
+ import IconButton from '@material-ui/core/IconButton';
3
+ import Menu from '@material-ui/core/Menu';
4
+ import MenuItem from '@material-ui/core/MenuItem';
5
+ import Toolbar from '@material-ui/core/Toolbar';
6
+ import Typography from '@material-ui/core/Typography';
7
+ import MenuIcon from '@material-ui/icons/Menu';
8
+ import * as React from 'react';
9
+ import { Link, Route } from "react-router-dom";
10
+ import './App.css';
11
+ import Models from './Models';
12
+ import SchedulableRecords from './SchedulableRecords';
13
+ import Signals from './Signals';
14
+ import Workers from './Workers';
15
+
16
+ interface IAppState {
17
+ menuAnchorEl: HTMLElement | null
18
+ }
19
+
20
+ class App extends React.Component<any, IAppState> {
21
+ private workersTitleRender: () => JSX.Element;
22
+ private signalsTitleRender: () => JSX.Element;
23
+ private modelsTitleRender: () => JSX.Element;
24
+ private schedulableRecordsTitleRender: (props: any) => JSX.Element;
25
+ private schedulableRecordsRender: (props: any) => JSX.Element;
26
+
27
+ public constructor(props: any) {
28
+ super(props);
29
+ this.handleMenuButtonClick = this.handleMenuButtonClick.bind(this);
30
+ this.handleMenuClose = this.handleMenuClose.bind(this);
31
+ this.state = {menuAnchorEl: null};
32
+ this.workersTitleRender = () => (
33
+ <Typography variant="title" color="inherit">Workers</Typography>
34
+ )
35
+ this.signalsTitleRender = () => (
36
+ <Typography variant="title" color="inherit">Signals</Typography>
37
+ )
38
+ this.modelsTitleRender = () => (
39
+ <Typography variant="title" color="inherit">Models</Typography>
40
+ )
41
+ this.schedulableRecordsTitleRender = ({ match }) => (
42
+ <Typography variant="title" color="inherit">{match.params.name}</Typography>
43
+ )
44
+ this.schedulableRecordsRender = ({ match }) => (
45
+ <SchedulableRecords model_name={match.params.name} />
46
+ )
47
+ }
48
+
49
+ public handleMenuButtonClick(event: any) {
50
+ this.setState({menuAnchorEl: event.currentTarget});
51
+ }
52
+
53
+ public handleMenuClose() {
54
+ this.setState({menuAnchorEl: null});
55
+ }
56
+
57
+ public render() {
58
+ const { menuAnchorEl }= this.state;
59
+
60
+ return (
61
+ <div className="main">
62
+ <AppBar position="static">
63
+ <Toolbar>
64
+ <IconButton className="menu" color="inherit" aria-label="Menu" onClick={this.handleMenuButtonClick}>
65
+ <MenuIcon />
66
+ </IconButton>
67
+ <Menu id="nav-menu" anchorEl={menuAnchorEl} open={Boolean(menuAnchorEl)} onClose={this.handleMenuClose}>
68
+ <MenuItem><Link to="/workers" onClick={this.handleMenuClose}>Workers</Link></MenuItem>
69
+ <MenuItem><Link to="/signals" onClick={this.handleMenuClose}>Signals</Link></MenuItem>
70
+ <MenuItem><Link to="/models" onClick={this.handleMenuClose}>Models</Link></MenuItem>
71
+ </Menu>
72
+
73
+ <Route path="/workers" render={this.workersTitleRender} />
74
+ <Route path="/signals" render={this.signalsTitleRender} />
75
+ <Route path="/models/:name" render={this.schedulableRecordsTitleRender} />
76
+ <Route exact={true} path="/models" render={this.modelsTitleRender} />
77
+ </Toolbar>
78
+ </AppBar>
79
+
80
+ <div className="content" style={{"padding": "15px"}}>
81
+ <Route path="/workers" component={Workers} />
82
+ <Route path="/signals" component={Signals} />
83
+ <Route path="/models/:name" render={this.schedulableRecordsRender} />
84
+ <Route exact={true} path="/models" component={Models} />
85
+ </div>
86
+ </div>
87
+ );
88
+ }
89
+ }
90
+
91
+ export default App;
@@ -0,0 +1,61 @@
1
+ import Paper from '@material-ui/core/Paper';
2
+ import Table from '@material-ui/core/Table';
3
+ import TableBody from '@material-ui/core/TableBody';
4
+ import TableCell from '@material-ui/core/TableCell';
5
+ import TableHead from '@material-ui/core/TableHead';
6
+ import TableRow from '@material-ui/core/TableRow';
7
+ import * as React from 'react';
8
+ import { Link } from "react-router-dom";
9
+
10
+ import { IGlobalWindow } from './interfaces';
11
+
12
+ declare var window: IGlobalWindow
13
+
14
+ interface IModelsState {
15
+ models: string[]
16
+ }
17
+
18
+ class Models extends React.Component<any, IModelsState> {
19
+ constructor(props: any) {
20
+ super(props)
21
+ this.state = {models: []}
22
+ }
23
+
24
+ public componentDidMount() {
25
+ this.fetchModels();
26
+ }
27
+
28
+ public fetchModels(): void {
29
+ const that = this;
30
+ fetch(`${window.mountPath}/models.json`)
31
+ .then((res) => res.json())
32
+ .then((data) => {
33
+ that.setState(data);
34
+ }).catch((err) => {
35
+ console.error(err);
36
+ });
37
+ }
38
+
39
+ public render() {
40
+ return (
41
+ <Paper className="models-container">
42
+ <Table className="models">
43
+ <TableHead>
44
+ <TableRow>
45
+ <TableCell>Model Name</TableCell>
46
+ </TableRow>
47
+ </TableHead>
48
+ <TableBody>
49
+ {this.state.models.map((model) => (
50
+ <TableRow key={model}>
51
+ <TableCell><Link to={`/models/${model}`}>{model}</Link></TableCell>
52
+ </TableRow>
53
+ ))}
54
+ </TableBody>
55
+ </Table>
56
+ </Paper>
57
+ )
58
+ }
59
+ }
60
+
61
+ export default Models;
@@ -0,0 +1,208 @@
1
+ import Button from '@material-ui/core/Button';
2
+ import Chip from '@material-ui/core/Chip';
3
+ import Modal from '@material-ui/core/Modal';
4
+ import Paper from '@material-ui/core/Paper';
5
+ import Snackbar from '@material-ui/core/Snackbar';
6
+ import TableRow from '@material-ui/core/TableRow';
7
+ import Typography from '@material-ui/core/Typography';
8
+ import classNames from 'classnames';
9
+ import { format, parse } from 'date-fns';
10
+ import * as React from 'react';
11
+ import SyntaxHighligher from 'react-syntax-highlighter';
12
+ import { dark } from 'react-syntax-highlighter/styles/hljs';
13
+
14
+ import { IGlobalWindow, ISchedulableRecordProps } from './interfaces';
15
+ import SchedulableRecordTableCell from './SchedulableRecordTableCell';
16
+
17
+ declare var window: IGlobalWindow
18
+
19
+ class SchedulableRecord extends React.Component<ISchedulableRecordProps, any> {
20
+ private statusChipColors: object = {
21
+ locked: "secondary",
22
+ not_scheduled: "default",
23
+ waiting: "primary",
24
+ };
25
+
26
+ constructor(props: ISchedulableRecordProps) {
27
+ super(props)
28
+
29
+ this.state = {
30
+ detailModalOpen: false,
31
+ notificationMessage: <span />,
32
+ notificationOpen: false,
33
+ }
34
+ }
35
+
36
+ public render() {
37
+ const record = this.props.record;
38
+ const rowClassNames = classNames({
39
+ "late": this.isLate(),
40
+ "too-late": this.isTooLate(),
41
+ });
42
+
43
+ return (
44
+ <TableRow key={record.id} className={rowClassNames} style={this.rowStyle()}>
45
+ <SchedulableRecordTableCell><Chip label={record.crono_trigger_status} color={this.statusChipColors[record.crono_trigger_status]}/></SchedulableRecordTableCell>
46
+ <SchedulableRecordTableCell>{record.id}</SchedulableRecordTableCell>
47
+ <SchedulableRecordTableCell>{record.cron}</SchedulableRecordTableCell>
48
+ <SchedulableRecordTableCell>{this.formatTime(record.next_execute_at)}</SchedulableRecordTableCell>
49
+ <SchedulableRecordTableCell>{record.delay_sec}</SchedulableRecordTableCell>
50
+ <SchedulableRecordTableCell>{record.execute_lock}</SchedulableRecordTableCell>
51
+ <SchedulableRecordTableCell>{record.time_to_unlock}</SchedulableRecordTableCell>
52
+ <SchedulableRecordTableCell>{this.formatTime(record.last_executed_at)}</SchedulableRecordTableCell>
53
+ <SchedulableRecordTableCell>{record.locked_by}</SchedulableRecordTableCell>
54
+ <SchedulableRecordTableCell>{this.formatTime(record.last_error_time)}</SchedulableRecordTableCell>
55
+ <SchedulableRecordTableCell>{record.retry_count}</SchedulableRecordTableCell>
56
+ <SchedulableRecordTableCell>
57
+ <Button variant="contained" style={{"marginRight": "8px"}} onClick={this.handleDetailClick}>Detail</Button>
58
+
59
+ <Modal
60
+ aria-labelledby={`schedulable-record-modal-title-${record.id}`}
61
+ open={this.state.detailModalOpen}
62
+ onClose={this.handleDetailModalClose}
63
+ style={{display: "flex", alignItems: "center", justifyContent: "center"}}
64
+ >
65
+ <Paper className="schedulable-record-modal" style={{width: "600px", padding: "8px"}}>
66
+ <Typography variant="title" id={`schedulable-record-modal-title-${record.id}`}>
67
+ {this.props.model_name}: {record.id}
68
+ </Typography>
69
+ <SyntaxHighligher language="json" style={dark}>
70
+ {JSON.stringify(record, null, " ")}
71
+ </SyntaxHighligher>
72
+ </Paper>
73
+ </Modal>
74
+ </SchedulableRecordTableCell>
75
+ <SchedulableRecordTableCell>
76
+ <Button variant="contained" style={{"marginRight": "8px"}} onClick={this.handleUnlockClick}>Unlock</Button>
77
+ <Button variant="contained" style={{"marginRight": "8px"}} onClick={this.handleRetryClick}>Retry</Button>
78
+ <Button variant="contained" color="secondary" onClick={this.handleResetClick}>Reset</Button>
79
+
80
+ <Snackbar
81
+ anchorOrigin={{vertical: "bottom", horizontal: "right"}}
82
+ open={this.state.notificationOpen}
83
+ autoHideDuration={3000}
84
+ onClose={this.handleNotificationClose}
85
+ message={this.state.notificationMessage}
86
+ />
87
+ </SchedulableRecordTableCell>
88
+ </TableRow>
89
+ )
90
+ }
91
+
92
+ private formatTime(iso8601: string | null): string {
93
+ if (iso8601 === null) {
94
+ return "";
95
+ }
96
+ const date = parse(iso8601);
97
+ return format(date, "YYYY/MM/DD (ddd) HH:mm:ss Z");
98
+ }
99
+
100
+ private isLate(): boolean {
101
+ const record = this.props.record;
102
+ return record.delay_sec > 60 && record.delay_sec <= 180;
103
+ }
104
+
105
+ private isTooLate(): boolean {
106
+ const record = this.props.record;
107
+ return record.delay_sec > 180;
108
+ }
109
+
110
+ private rowStyle(): object {
111
+ if (this.isLate()) {
112
+ return {backgroundColor: "#FFEA00"}
113
+ } else if (this.isTooLate()) {
114
+ return {backgroundColor: "#C62828"}
115
+ } else {
116
+ return {}
117
+ }
118
+ }
119
+
120
+ private handleUnlockClick = (event: any) => {
121
+ const record = this.props.record;
122
+ fetch(`${window.mountPath}/models/${this.props.model_name}/${record.id}/unlock`, {
123
+ headers: {"content-type": "application/json"},
124
+ method: "POST"
125
+ }).then(this.handleResponseStatus).then((res) => {
126
+ this.setState({
127
+ ...this.state,
128
+ notificationMessage: <span>Unlock id:{record.id}</span>,
129
+ notificationOpen: true,
130
+ })
131
+ }).catch((err) => {
132
+ this.setState({
133
+ ...this.state,
134
+ notificationMessage: <span>Failed to unlock ({err.message})</span>,
135
+ notificationOpen: true,
136
+ })
137
+ })
138
+ }
139
+
140
+ private handleRetryClick = (event: any) => {
141
+ const record = this.props.record;
142
+ fetch(`${window.mountPath}/models/${this.props.model_name}/${record.id}/retry`, {
143
+ headers: {"content-type": "application/json"},
144
+ method: "POST"
145
+ }).then(this.handleResponseStatus).then((res) => {
146
+ this.setState({
147
+ notificationMessage: <span>Retry id:{record.id}</span>,
148
+ notificationOpen: true,
149
+ })
150
+ }).catch((err) => {
151
+ this.setState({
152
+ notificationMessage: <span>Failed to retry ({err.message})</span>,
153
+ notificationOpen: true,
154
+ })
155
+ })
156
+ }
157
+
158
+ private handleResetClick = (event: any) => {
159
+ const record = this.props.record;
160
+ fetch(`${window.mountPath}/models/${this.props.model_name}/${record.id}/reset`, {
161
+ headers: {"content-type": "application/json"},
162
+ method: "POST"
163
+ }).then(this.handleResponseStatus).then((res) => {
164
+ this.setState({
165
+ notificationMessage: <span>Reset id:{record.id}</span>,
166
+ notificationOpen: true,
167
+ })
168
+ }).catch((err) => {
169
+ this.setState({
170
+ notificationMessage: <span>Failed to reset ({err.message})</span>,
171
+ notificationOpen: true,
172
+ })
173
+ })
174
+ }
175
+
176
+ private handleResponseStatus = (res: Response) => {
177
+ if (!res.ok) {
178
+ return res.json().then((data: any) => {
179
+ throw new Error(data.error);
180
+ })
181
+ } else {
182
+ return Promise.resolve(res);
183
+ }
184
+ }
185
+
186
+ private handleNotificationClose = (ev: any, reason: any) => {
187
+ this.setState({
188
+ ...this.state,
189
+ notificationOpen: false,
190
+ })
191
+ }
192
+
193
+ private handleDetailClick = (ev: any) => {
194
+ this.setState({
195
+ ...this.state,
196
+ detailModalOpen: true,
197
+ })
198
+ }
199
+
200
+ private handleDetailModalClose = (ev: any) => {
201
+ this.setState({
202
+ ...this.state,
203
+ detailModalOpen: false,
204
+ })
205
+ }
206
+ }
207
+
208
+ export default SchedulableRecord;