diff --git a/web/package-lock.json b/web/package-lock.json index f11e2ef0f0..ed9433ec49 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -9,12 +9,14 @@ "version": "1.0.0", "license": "Apache-2.0", "dependencies": { + "@date-io/moment": "^1.3.13", "@fortawesome/fontawesome-svg-core": "^1.2.32", "@fortawesome/free-solid-svg-icons": "^5.14.0", "@fortawesome/react-fontawesome": "^0.1.12", "@material-ui/core": "^4.12.3", "@material-ui/icons": "^4.11.2", "@material-ui/lab": "^4.0.0-alpha.56", + "@material-ui/pickers": "^3.3.10", "@material-ui/styles": "^4.11.4", "@types/react-router-dom": "^5.1.2", "@types/react-syntax-highlighter": "^13.5.2", @@ -37,6 +39,7 @@ "d3-scale": "^3.2.1", "d3-selection": "^1.4.1", "dagre": "^0.8.5", + "file-saver": "^2.0.5", "http-proxy-middleware": "^0.20.0", "lodash": "^4.17.21", "moment": "^2.29.4", @@ -45,6 +48,7 @@ "react": "^16.8.0", "react-dom": "^16.8.0", "react-helmet-async": "^1.3.0", + "react-inlinesvg": "^3.0.1", "react-redux": "^6.0.1", "react-router-dom": "^5.1.2", "react-syntax-highlighter": "^15.4.4", @@ -63,6 +67,7 @@ "@types/dagre": "^0.7.44", "@types/enzyme": "^3.1.16", "@types/enzyme-adapter-react-16": "^1.0.3", + "@types/file-saver": "^2.0.5", "@types/jest": "^27.5.2", "@types/lodash": "^4.14.123", "@types/material-ui": "^0.21.7", @@ -635,6 +640,22 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@date-io/core": { + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@date-io/core/-/core-1.3.13.tgz", + "integrity": "sha512-AlEKV7TxjeK+jxWVKcCFrfYAk8spX9aCyiToFIiLPtfQbsjmRGLIhb5VZgptQcJdHtLXo7+m0DuurwFgUToQuA==" + }, + "node_modules/@date-io/moment": { + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@date-io/moment/-/moment-1.3.13.tgz", + "integrity": "sha512-3kJYusJtQuOIxq6byZlzAHoW/18iExJer9qfRF5DyyzdAk074seTuJfdofjz4RFfTd/Idk8WylOQpWtERqvFuQ==", + "dependencies": { + "@date-io/core": "^1.3.13" + }, + "peerDependencies": { + "moment": "^2.24.0" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -1734,6 +1755,27 @@ } } }, + "node_modules/@material-ui/pickers": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/@material-ui/pickers/-/pickers-3.3.10.tgz", + "integrity": "sha512-hS4pxwn1ZGXVkmgD4tpFpaumUaAg2ZzbTrxltfC5yPw4BJV+mGkfnQOB4VpWEYZw2jv65Z0wLwDE/piQiPPZ3w==", + "deprecated": "Material UI Pickers v3 doesn't receive active development since January 2020. See the guide https://mui.com/material-ui/guides/pickers-migration/ to upgrade.", + "dependencies": { + "@babel/runtime": "^7.6.0", + "@date-io/core": "1.x", + "@types/styled-jsx": "^2.2.8", + "clsx": "^1.0.2", + "react-transition-group": "^4.0.0", + "rifm": "^0.7.0" + }, + "peerDependencies": { + "@date-io/core": "^1.3.6", + "@material-ui/core": "^4.0.0", + "prop-types": "^15.6.0", + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + } + }, "node_modules/@material-ui/styles": { "version": "4.11.5", "resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.5.tgz", @@ -2374,6 +2416,12 @@ "@types/range-parser": "*" } }, + "node_modules/@types/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ==", + "dev": true + }, "node_modules/@types/geojson": { "version": "7946.0.10", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz", @@ -2686,6 +2734,14 @@ "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", "dev": true }, + "node_modules/@types/styled-jsx": { + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/@types/styled-jsx/-/styled-jsx-2.2.9.tgz", + "integrity": "sha512-W/iTlIkGEyTBGTEvZCey8EgQlQ5l0DwMqi3iOXlLs2kyBwYTXHKEiU6IZ5EwoRwngL8/dGYuzezSup89ttVHLw==", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/unist": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", @@ -6736,6 +6792,11 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/exenv": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", + "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==" + }, "node_modules/exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", @@ -7265,6 +7326,11 @@ "node": ">=8.9.0" } }, + "node_modules/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" + }, "node_modules/fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", @@ -12763,6 +12829,14 @@ "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" }, + "node_modules/react-from-dom": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/react-from-dom/-/react-from-dom-0.6.2.tgz", + "integrity": "sha512-qvWWTL/4xw4k/Dywd41RBpLQUSq97csuv15qrxN+izNeLYlD9wn5W8LspbfYe5CWbaSdkZ72BsaYBPQf2x4VbQ==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-helmet-async": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/react-helmet-async/-/react-helmet-async-1.3.0.tgz", @@ -12779,6 +12853,18 @@ "react-dom": "^16.6.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-inlinesvg": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/react-inlinesvg/-/react-inlinesvg-3.0.1.tgz", + "integrity": "sha512-cBfoyfseNI2PkDA7ZKIlDoHq0eMfpoC3DhKBQNC+/X1M4ZQB+aXW+YiNPUDDDKXUsGDUIZWWiZWNFeauDIVdoA==", + "dependencies": { + "exenv": "^1.2.2", + "react-from-dom": "^0.6.2" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -13336,6 +13422,17 @@ "node": ">=0.10.0" } }, + "node_modules/rifm": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/rifm/-/rifm-0.7.0.tgz", + "integrity": "sha512-DSOJTWHD67860I5ojetXdEQRIBvF6YcpNe53j0vn1vp9EUb9N80EiZTxgP+FkDKorWC8PZw052kTF4C1GOivCQ==", + "dependencies": { + "@babel/runtime": "^7.3.1" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, "node_modules/rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", @@ -16421,6 +16518,19 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "@date-io/core": { + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@date-io/core/-/core-1.3.13.tgz", + "integrity": "sha512-AlEKV7TxjeK+jxWVKcCFrfYAk8spX9aCyiToFIiLPtfQbsjmRGLIhb5VZgptQcJdHtLXo7+m0DuurwFgUToQuA==" + }, + "@date-io/moment": { + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@date-io/moment/-/moment-1.3.13.tgz", + "integrity": "sha512-3kJYusJtQuOIxq6byZlzAHoW/18iExJer9qfRF5DyyzdAk074seTuJfdofjz4RFfTd/Idk8WylOQpWtERqvFuQ==", + "requires": { + "@date-io/core": "^1.3.13" + } + }, "@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -17225,6 +17335,19 @@ "react-is": "^16.8.0 || ^17.0.0" } }, + "@material-ui/pickers": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/@material-ui/pickers/-/pickers-3.3.10.tgz", + "integrity": "sha512-hS4pxwn1ZGXVkmgD4tpFpaumUaAg2ZzbTrxltfC5yPw4BJV+mGkfnQOB4VpWEYZw2jv65Z0wLwDE/piQiPPZ3w==", + "requires": { + "@babel/runtime": "^7.6.0", + "@date-io/core": "1.x", + "@types/styled-jsx": "^2.2.8", + "clsx": "^1.0.2", + "react-transition-group": "^4.0.0", + "rifm": "^0.7.0" + } + }, "@material-ui/styles": { "version": "4.11.5", "resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.5.tgz", @@ -17800,6 +17923,12 @@ "@types/range-parser": "*" } }, + "@types/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ==", + "dev": true + }, "@types/geojson": { "version": "7946.0.10", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz", @@ -18114,6 +18243,14 @@ "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", "dev": true }, + "@types/styled-jsx": { + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/@types/styled-jsx/-/styled-jsx-2.2.9.tgz", + "integrity": "sha512-W/iTlIkGEyTBGTEvZCey8EgQlQ5l0DwMqi3iOXlLs2kyBwYTXHKEiU6IZ5EwoRwngL8/dGYuzezSup89ttVHLw==", + "requires": { + "@types/react": "*" + } + }, "@types/unist": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", @@ -21267,6 +21404,11 @@ "strip-final-newline": "^2.0.0" } }, + "exenv": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", + "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==" + }, "exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", @@ -21697,6 +21839,11 @@ } } }, + "file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" + }, "fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", @@ -25832,6 +25979,12 @@ "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" }, + "react-from-dom": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/react-from-dom/-/react-from-dom-0.6.2.tgz", + "integrity": "sha512-qvWWTL/4xw4k/Dywd41RBpLQUSq97csuv15qrxN+izNeLYlD9wn5W8LspbfYe5CWbaSdkZ72BsaYBPQf2x4VbQ==", + "requires": {} + }, "react-helmet-async": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/react-helmet-async/-/react-helmet-async-1.3.0.tgz", @@ -25844,6 +25997,15 @@ "shallowequal": "^1.1.0" } }, + "react-inlinesvg": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/react-inlinesvg/-/react-inlinesvg-3.0.1.tgz", + "integrity": "sha512-cBfoyfseNI2PkDA7ZKIlDoHq0eMfpoC3DhKBQNC+/X1M4ZQB+aXW+YiNPUDDDKXUsGDUIZWWiZWNFeauDIVdoA==", + "requires": { + "exenv": "^1.2.2", + "react-from-dom": "^0.6.2" + } + }, "react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -26286,6 +26448,14 @@ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "dev": true }, + "rifm": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/rifm/-/rifm-0.7.0.tgz", + "integrity": "sha512-DSOJTWHD67860I5ojetXdEQRIBvF6YcpNe53j0vn1vp9EUb9N80EiZTxgP+FkDKorWC8PZw052kTF4C1GOivCQ==", + "requires": { + "@babel/runtime": "^7.3.1" + } + }, "rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", diff --git a/web/package.json b/web/package.json index 869f4ca23e..33c53966a7 100644 --- a/web/package.json +++ b/web/package.json @@ -17,12 +17,14 @@ "node": ">12.22.7" }, "dependencies": { + "@date-io/moment": "^1.3.13", "@fortawesome/fontawesome-svg-core": "^1.2.32", "@fortawesome/free-solid-svg-icons": "^5.14.0", "@fortawesome/react-fontawesome": "^0.1.12", "@material-ui/core": "^4.12.3", "@material-ui/icons": "^4.11.2", "@material-ui/lab": "^4.0.0-alpha.56", + "@material-ui/pickers": "^3.3.10", "@material-ui/styles": "^4.11.4", "@types/react-router-dom": "^5.1.2", "@types/react-syntax-highlighter": "^13.5.2", @@ -45,6 +47,7 @@ "d3-scale": "^3.2.1", "d3-selection": "^1.4.1", "dagre": "^0.8.5", + "file-saver": "^2.0.5", "http-proxy-middleware": "^0.20.0", "lodash": "^4.17.21", "moment": "^2.29.4", @@ -53,6 +56,7 @@ "react": "^16.8.0", "react-dom": "^16.8.0", "react-helmet-async": "^1.3.0", + "react-inlinesvg": "^3.0.1", "react-redux": "^6.0.1", "react-router-dom": "^5.1.2", "react-syntax-highlighter": "^15.4.4", @@ -71,6 +75,7 @@ "@types/dagre": "^0.7.44", "@types/enzyme": "^3.1.16", "@types/enzyme-adapter-react-16": "^1.0.3", + "@types/file-saver": "^2.0.5", "@types/jest": "^27.5.2", "@types/lodash": "^4.14.123", "@types/material-ui": "^0.21.7", diff --git a/web/setupProxy.js b/web/setupProxy.js index 1ad2081a50..cb97ac359e 100644 --- a/web/setupProxy.js +++ b/web/setupProxy.js @@ -10,6 +10,7 @@ const path = __dirname + '/dist' app.use('/', express.static(path)) app.use('/datasets', express.static(path)) +app.use('/events', express.static(path)) app.use('/lineage/:type/:namespace/:name', express.static(path)) app.use(proxy('/api/v1', apiOptions)) diff --git a/web/src/components/App.tsx b/web/src/components/App.tsx index 4d1f278f2b..ffe6ac7024 100644 --- a/web/src/components/App.tsx +++ b/web/src/components/App.tsx @@ -3,6 +3,7 @@ import { Box, Container, CssBaseline } from '@material-ui/core' import { ConnectedRouter, routerMiddleware } from 'connected-react-router' import { Helmet, HelmetProvider } from 'react-helmet-async' +import { MuiPickersUtilsProvider } from '@material-ui/pickers' import { MuiThemeProvider } from '@material-ui/core/styles' import { Provider } from 'react-redux' import { Route, Switch } from 'react-router-dom' @@ -12,9 +13,11 @@ import { createBrowserHistory } from 'history' import { theme } from '../helpers/theme' import BottomBar from './bottom-bar/BottomBar' import Datasets from '../routes/datasets/Datasets' +import Events from '../routes/events/Events' import Header from './header/Header' import Jobs from '../routes/jobs/Jobs' import Lineage from './lineage/Lineage' +import MomentUtils from '@date-io/moment' import React, { ReactElement } from 'react' import Sidenav from './sidenav/Sidenav' import Toast from './Toast' @@ -45,29 +48,34 @@ const App = (): ReactElement => { - - {TITLE} - - - - - - - - - - - - - - - - - - - - - + + + {TITLE} + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/src/components/core/code/MqJson.tsx b/web/src/components/core/code/MqJson.tsx index a58015d240..fbeb68763b 100644 --- a/web/src/components/core/code/MqJson.tsx +++ b/web/src/components/core/code/MqJson.tsx @@ -8,13 +8,17 @@ import SyntaxHighlighter from 'react-syntax-highlighter' interface OwnProps { code: object + showLineNumbers?: boolean + wrapLongLines?: boolean } -const MqJson: React.FC = ({ code }) => { +const MqJson: React.FC = ({ code, wrapLongLines = false, showLineNumbers = false }) => { return ( + createStyles({ + root: { + minWidth: '200px', + cursor: 'pointer', + backgroundColor: 'transparent', + border: `2px solid ${theme.palette.common.white}`, + padding: `${theme.spacing(1)}px ${theme.spacing(1)}px`, + transition: theme.transitions.create(['border-color', 'box-shadow']), + '& *': { + cursor: 'pointer' + }, + '&:hover': { + borderColor: theme.palette.primary.main, + boxShadow: `${alpha(theme.palette.primary.main, 0.25)} 0 0 0 3px`, + '& > label': { + color: theme.palette.primary.main, + transition: theme.transitions.create(['color']) + } + }, + '& > label': { + top: 'initial', + left: 'initial' + }, + '& > div': { + marginTop: theme.spacing(1), + '&:before': { + display: 'none' + }, + '&:after': { + display: 'none' + }, + '& > input': { + paddingBottom: 0 + } + } + } + }) + +interface OwnProps { + value: string + onChange: (e: any) => void + label?: string + format?: string +} + +type DatePickerProps = WithStyles & OwnProps + +class MqDatePicker extends React.Component { + render() { + const { classes, value, onChange, label = '', format = "MMM DD yyyy hh:mm a" } = this.props + return ( + + ) + } +} + +export default withStyles(styles)(MqDatePicker) \ No newline at end of file diff --git a/web/src/components/core/icon-button/MqIconButton.tsx b/web/src/components/core/icon-button/MqIconButton.tsx index c19ffeb3a6..4d4e39dd66 100644 --- a/web/src/components/core/icon-button/MqIconButton.tsx +++ b/web/src/components/core/icon-button/MqIconButton.tsx @@ -13,7 +13,6 @@ import withStyles, { WithStyles } from '@material-ui/core/styles/withStyles' const styles = (theme: Theme) => createStyles({ root: { - padding: theme.spacing(4), width: theme.spacing(8), height: theme.spacing(8), borderRadius: theme.spacing(2), diff --git a/web/src/components/sidenav/Sidenav.tsx b/web/src/components/sidenav/Sidenav.tsx index c1f7a14256..e06435fb6b 100644 --- a/web/src/components/sidenav/Sidenav.tsx +++ b/web/src/components/sidenav/Sidenav.tsx @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 import React from 'react' +import SVG from 'react-inlinesvg' import createStyles from '@material-ui/core/styles/createStyles' import withStyles, { WithStyles } from '@material-ui/core/styles/withStyles' @@ -70,6 +71,19 @@ class Sidenav extends React.Component { + + + + + + {/* todo remove this link for now until direct linking available */} {/**/} {/* { return `${dateString.slice(0, -2)}${dateString.slice(-2).toLowerCase()}` } } + +export const fileSize = (data: string) => { + const size = encodeURI(data).split(/%..|./).length - 1; + return { + kiloBytes: size / 1024, + megaBytes: (size / 1024) / 1024 + } +} \ No newline at end of file diff --git a/web/src/helpers/time.ts b/web/src/helpers/time.ts index 40d75a0c5f..637542695a 100644 --- a/web/src/helpers/time.ts +++ b/web/src/helpers/time.ts @@ -26,3 +26,11 @@ export function stopWatchDuration(durationMs: number) { return `${duration.asMilliseconds()} ms` } } + +export function formatDatePicker(val: string) { + return moment(val).format("YYYY-MM-DDTHH:mm:ss") +} + +export function formatDateAPIQuery(val: string) { + return moment(val).format("YYYY-MM-DDTHH:mm:ss[.000Z]") +} \ No newline at end of file diff --git a/web/src/img/iconSearchArrow.svg b/web/src/img/iconSearchArrow.svg new file mode 100644 index 0000000000..6a47341ddb --- /dev/null +++ b/web/src/img/iconSearchArrow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/routes/events/Events.tsx b/web/src/routes/events/Events.tsx new file mode 100644 index 0000000000..0976ca4192 --- /dev/null +++ b/web/src/routes/events/Events.tsx @@ -0,0 +1,316 @@ +// SPDX-License-Identifier: Apache-2.0 + +import React from 'react' +import * as Redux from 'redux' +import moment from 'moment' +import { Theme, Container, Table, TableBody, TableCell, TableHead, TableRow, Button } from '@material-ui/core' +import { Event } from '../../types/api' +import { formatDatePicker, formatDateAPIQuery } from '../../helpers/time' +import { IState } from '../../store/reducers' +import { MqScreenLoad } from '../../components/core/screen-load/MqScreenLoad' +import { bindActionCreators } from 'redux' +import { connect } from 'react-redux' +import { theme } from '../../helpers/theme' +import { fetchEvents, resetEvents } from '../../store/actionCreators' +import { formatUpdatedAt, fileSize } from '../../helpers' +import { saveAs } from 'file-saver' +import Box from '@material-ui/core/Box' +import MqDatePicker from '../../components/core/date-picker/MqDatePicker' +import MqEmpty from '../../components/core/empty/MqEmpty' +import MqText from '../../components/core/text/MqText' +import MqJson from '../../components/core/code/MqJson' +import createStyles from '@material-ui/core/styles/createStyles' +import withStyles, { WithStyles } from '@material-ui/core/styles/withStyles' + +const styles = (theme: Theme) => { + return createStyles({ + nav: { + display: 'flex', + alignItems: 'center', + gap: theme.spacing(2) + }, + type: { + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1) + }, + status: { + width: theme.spacing(2), + height: theme.spacing(2), + borderRadius: '50%' + }, + table: { + marginBottom: '100px', + }, + row: { + cursor: 'pointer', + '&:hover': { + backgroundColor: theme.palette.action.hover, + } + } + }) +} + +interface StateProps { + events: Event[] + isEventsLoading: boolean + isEventsInit: boolean +} + +interface EventsState { + events: Event[] + rowExpanded: number | null + dateFrom: string + dateTo: string + page: number + pageIsLast: boolean +} + +interface DispatchProps { + fetchEvents: typeof fetchEvents + resetEvents: typeof resetEvents +} + +type EventsProps = WithStyles & StateProps & DispatchProps + +const EVENTS_COLUMNS = ['ID', 'STATE', 'NAME', 'NAMESPACE', 'TIME'] + +function eventTypeColor(type: string) { + switch (type) { + case 'START': + return theme.palette.info.main + case 'COMPLETE': + return theme.palette.primary.main + } +} + +class Events extends React.Component { + pageSize: number + + constructor(props: EventsProps) { + super(props) + this.state = { + page: 1, + events: [], + rowExpanded: null, + pageIsLast: false, + dateFrom: formatDateAPIQuery(moment().startOf('day').toString()), + dateTo: formatDateAPIQuery(moment().endOf('day').toString()) + } + this.pageSize = 20 + } + + componentDidMount() { + const { dateFrom, dateTo } = this.state + this.props.fetchEvents(dateFrom, dateTo, this.pageSize) + } + + componentDidUpdate() { + const { events: eventsState, page } = this.state + const { events: eventsProps } = this.props + + if (eventsProps !== eventsState) { + this.setState({ + events: eventsProps, + pageIsLast: eventsProps.length < page * this.pageSize ? true : false + }) + } + } + + componentWillUnmount() { + this.props.resetEvents() + } + + getEvents() { + const { events, page } = this.state + return events.slice(0 + ((page - 1) * this.pageSize), this.pageSize + ((page - 1) * this.pageSize)) + } + + pageNavigation() { + const { events, page } = this.state + const titlePos = events.length ? `${this.pageSize * page - this.pageSize} - ${events.length}` : `${events.length}` + return `${page} (${titlePos})` + } + + handleChangeDatepicker(e: any, direction: 'from' | 'to') { + const { dateFrom, dateTo } = this.state + const isDirectionFrom = direction === 'from' + const keyDate = isDirectionFrom ? 'dateFrom' : 'dateTo' + + this.props.fetchEvents( + formatDateAPIQuery(isDirectionFrom ? e.toDate() : dateFrom), + formatDateAPIQuery(isDirectionFrom ? dateTo : e.toDate()), + this.pageSize + ) + + this.setState({ [keyDate]: formatDatePicker(e.toDate()), page: 1, rowExpanded: null } as any) + } + + handleClickPage(direction: 'prev' | 'next') { + const { dateFrom, dateTo, page } = this.state + const directionPage = direction === 'next' ? page + 1 : page - 1 + + this.props.fetchEvents(formatDateAPIQuery(dateFrom), formatDateAPIQuery(dateTo), this.pageSize * directionPage) + this.setState({ page: directionPage, rowExpanded: null }) + } + + handleDownloadPayload(data: Event) { + let title = `${data.job.name}-${data.eventType}-${data.run.runId}` + let blob = new Blob([JSON.stringify(data)], { type: 'application/json' }) + saveAs(blob, `${title}.json`) + } + + render() { + const { classes, isEventsLoading, isEventsInit } = this.props + const { events, rowExpanded, page, dateFrom, dateTo, pageIsLast } = this.state + + return ( + + + <> + + EVENTS + Page: {this.pageNavigation()} + + + this.handleChangeDatepicker(e, 'from')} + /> + this.handleChangeDatepicker(e, 'to')} + /> + this.handleClickPage('prev')} + > + Previous page + + this.handleClickPage('next')} + > + Next page + + + {events.length === 0 ? ( + + + + Try changing dates or consulting our documentation to add events. + + + + ) : ( + <> + + + + {EVENTS_COLUMNS.map(field => { + return ( + + {field} + + ) + })} + + + + {this.getEvents().map((event, key: number) => { + return ( + + { + this.setState({ rowExpanded: key === rowExpanded ? null : key }) + }} + > + + {event.run.runId} + + + + + {event.eventType} + + + + {event.job.name} + + + {event.job.namespace} + + + {formatUpdatedAt(event.eventTime)} + + + {rowExpanded === key && + + + {fileSize(JSON.stringify(event)).kiloBytes > 500 ? ( + + + + + Please click on button and download payload as file + + + this.handleDownloadPayload(event)} + > + Download payload + + + + + ) : ( + + )} + + + } + + ) + })} + + + > + )} + > + + + ) + } +} + +const mapStateToProps = (state: IState) => ({ + events: state.events.result, + isEventsLoading: state.events.isLoading, + isEventsInit: state.events.init +}) + +const mapDispatchToProps = (dispatch: Redux.Dispatch) => + bindActionCreators( + { + fetchEvents: fetchEvents, + resetEvents: resetEvents + }, + dispatch + ) + +export default connect( + mapStateToProps, + mapDispatchToProps +)(withStyles(styles)(Events)) diff --git a/web/src/store/actionCreators/actionTypes.ts b/web/src/store/actionCreators/actionTypes.ts index 71611faf8b..64d6a62e9c 100644 --- a/web/src/store/actionCreators/actionTypes.ts +++ b/web/src/store/actionCreators/actionTypes.ts @@ -28,6 +28,11 @@ export const FETCH_DATASET_VERSIONS = 'FETCH_DATASET_VERSIONS' export const FETCH_DATASET_VERSIONS_SUCCESS = 'FETCH_DATASET_VERSIONS_SUCCESS' export const RESET_DATASET_VERSIONS = 'RESET_DATASET_VERSIONS' +// events +export const FETCH_EVENTS = 'FETCH_EVENTS' +export const FETCH_EVENTS_SUCCESS = 'FETCH_EVENTS_SUCCESS' +export const RESET_EVENTS = 'RESET_EVENTS' + // lineage export const FETCH_LINEAGE = 'FETCH_LINEAGE' export const FETCH_LINEAGE_SUCCESS = 'FETCH_LINEAGE_SUCCESS' diff --git a/web/src/store/actionCreators/index.ts b/web/src/store/actionCreators/index.ts index 06c18221c6..f888f19c42 100644 --- a/web/src/store/actionCreators/index.ts +++ b/web/src/store/actionCreators/index.ts @@ -2,9 +2,29 @@ import * as actionTypes from './actionTypes' -import { Dataset, DatasetVersion, Job, LineageGraph, Namespace, Run, Search } from '../../types/api' +import { Event, Dataset, DatasetVersion, Job, LineageGraph, Namespace, Run, Search } from '../../types/api' import { JobOrDataset } from '../../components/lineage/types' +export const fetchEvents = (after: string, before: string, limit: number) => ({ + type: actionTypes.FETCH_EVENTS, + payload: { + before, + after, + limit + } +}) + +export const fetchEventsSuccess = (events: Event[]) => ({ + type: actionTypes.FETCH_EVENTS_SUCCESS, + payload: { + events + } +}) + +export const resetEvents = () => ({ + type: actionTypes.RESET_EVENTS +}) + export const fetchDatasets = (namespace: string) => ({ type: actionTypes.FETCH_DATASETS, payload: { diff --git a/web/src/store/reducers/events.ts b/web/src/store/reducers/events.ts new file mode 100644 index 0000000000..13425856e2 --- /dev/null +++ b/web/src/store/reducers/events.ts @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 + +import { Event } from '../../types/api' +import { + FETCH_EVENTS, + FETCH_EVENTS_SUCCESS, + RESET_EVENTS +} from '../actionCreators/actionTypes' +import { fetchEventsSuccess } from '../actionCreators' + +export type IEventsState = { isLoading: boolean; result: Event[]; init: boolean } + +export const initialState: IEventsState = { isLoading: false, init: false, result: [] } + +type IEventsAction = ReturnType + +export default (state: IEventsState = initialState, action: IEventsAction): IEventsState => { + const { type, payload } = action + + switch (type) { + case FETCH_EVENTS: + return { ...state, isLoading: true } + case FETCH_EVENTS_SUCCESS: + return { ...state, isLoading: false, init: true, result: payload.events } + case RESET_EVENTS: + return initialState + default: + return state + } +} diff --git a/web/src/store/reducers/index.ts b/web/src/store/reducers/index.ts index 3bd5119e14..32220728c8 100644 --- a/web/src/store/reducers/index.ts +++ b/web/src/store/reducers/index.ts @@ -5,6 +5,7 @@ import { Reducer, combineReducers } from 'redux' import { connectRouter } from 'connected-react-router' import datasetVersions, { IDatasetVersionsState } from './datasetVersions' import datasets, { IDatasetsState } from './datasets' +import events, { IEventsState } from './events' import display, { IDisplayState } from './display' import jobs, { IJobsState } from './jobs' import lineage, { ILineageState } from './lineage' @@ -15,6 +16,7 @@ import search, { ISearchState } from './search' export interface IState { datasets: IDatasetsState datasetVersions: IDatasetVersionsState + events: IEventsState jobs: IJobsState runs: IRunsState namespaces: INamespacesState @@ -29,6 +31,7 @@ export default (history: History): Reducer => router: connectRouter(history), datasets, datasetVersions, + events, jobs, runs, namespaces, diff --git a/web/src/store/requests/events.ts b/web/src/store/requests/events.ts new file mode 100644 index 0000000000..4315aef6d7 --- /dev/null +++ b/web/src/store/requests/events.ts @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 + +import { API_URL } from '../../globals' +import { Events } from '../../types/api' +import { genericFetchWrapper } from './index' + +export const getEvents = async (after = '', before = '', limit = 100, sortDirection = 'desc') => { + const url = `${API_URL}/events/lineage?limit=${limit}&before=${before}&after=${after}&sortDirection=${sortDirection}` + return genericFetchWrapper(url, { method: 'GET' }, 'fetchEvents').then((r: Events) => { + return r.events.map((d) => ({ ...d })) + }) +} \ No newline at end of file diff --git a/web/src/store/requests/index.ts b/web/src/store/requests/index.ts index 16a10997d2..97fc2e4e54 100644 --- a/web/src/store/requests/index.ts +++ b/web/src/store/requests/index.ts @@ -40,5 +40,6 @@ export const genericFetchWrapper = async (url: string, params: IParams, function } export * from './datasets' +export * from './events' export * from './namespaces' export * from './jobs' diff --git a/web/src/store/sagas/index.ts b/web/src/store/sagas/index.ts index a2867f089f..0ed2578c4a 100644 --- a/web/src/store/sagas/index.ts +++ b/web/src/store/sagas/index.ts @@ -7,7 +7,8 @@ import { FETCH_JOBS, FETCH_LINEAGE, FETCH_RUNS, - FETCH_SEARCH + FETCH_SEARCH, + FETCH_EVENTS } from '../actionCreators/actionTypes' import { Namespaces } from '../../types/api' import { all, put, take } from 'redux-saga/effects' @@ -18,13 +19,14 @@ import { applicationError, fetchDatasetVersionsSuccess, fetchDatasetsSuccess, + fetchEventsSuccess, fetchJobsSuccess, fetchLineageSuccess, fetchNamespacesSuccess, fetchRunsSuccess, fetchSearchSuccess } from '../actionCreators' -import { getDatasetVersions, getDatasets, getJobs, getNamespaces, getRuns } from '../requests' +import { getDatasetVersions, getDatasets, getEvents, getJobs, getNamespaces, getRuns } from '../requests' import { getLineage } from '../requests/lineage' import { getSearch } from '../requests/search' @@ -98,6 +100,18 @@ export function* fetchDatasetsSaga() { } } +export function* fetchEventsSaga() { + while (true) { + try { + const { payload } = yield take(FETCH_EVENTS) + const events = yield call(getEvents, payload.after, payload.before, payload.limit) + yield put(fetchEventsSuccess(events)) + } catch (e) { + yield put(applicationError('Something went wrong while fetching event runs')) + } + } +} + export function* fetchDatasetVersionsSaga() { while (true) { try { @@ -117,6 +131,7 @@ export default function* rootSaga(): Generator { fetchRunsSaga(), fetchDatasetsSaga(), fetchDatasetVersionsSaga(), + fetchEventsSaga(), fetchLineage(), fetchSearch() ] diff --git a/web/src/types/api.ts b/web/src/types/api.ts index 320bf02fd2..1c95b14e2f 100644 --- a/web/src/types/api.ts +++ b/web/src/types/api.ts @@ -23,6 +23,36 @@ export interface Namespace { isHidden: boolean } +export interface Events { + events: Event[] +} + +export interface Event { + eventType: string + eventTime: string + producer: string + schemaURL: string + run: { + runId: string + facets: object + } + job: { + name: string + namespace: string + facets: object + } + inputs: { + name: string, + namespace: string, + facets: object + }[] + outputs: { + name: string, + namespace: string, + facets: object + }[] +} + export interface Datasets { datasets: Dataset[] }