crono_trigger 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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;