Skip to content

Commit 8edef22

Browse files
chore: Revamp Batch Prediction Jobs UI (#591)
<!-- Thanks for sending a pull request! Here are some tips for you: 1. Run unit tests and ensure that they are passing 2. If your change introduces any API changes, make sure to update the e2e tests 3. Make sure documentation is updated for your PR! --> # Description <!-- Briefly describe the motivation for the change. Please include illustrations where appropriate. --> This PR improves the UI/UX for the batch prediction job. It also upgrades the UI dependencies such as node version, react version, elasticsearch UI, and @caraml-ui/lib. This PR also do some clean up and refactoring such as: 1. Move the Batch Prediciton Job-related pages components to pages folder 2. Refactor previous JobConfig.js by breaking it down to smaller components # Modifications <!-- Summarize the key code changes. --> 1. On List Versions page, add View Batch Jobs button: <img width="768" alt="Screenshot 2024-06-12 at 09 32 23" src="https://github.com/caraml-dev/merlin/assets/8122852/71166cd0-aafe-4b1c-b4fe-a4530150f863"> 3. On Version Details page, add the list of Bath Prediction Jobs for the given model version <img width="768" alt="Screenshot 2024-06-12 at 09 36 20" src="https://github.com/caraml-dev/merlin/assets/8122852/9d0d1fe0-af0e-45a4-b8bd-1a80ae789971"> 4. On List Jobs page: a. Make model version columns clickable and link to the Version page b. Add Status filter <img width="768" alt="Screenshot 2024-06-12 at 09 34 00" src="https://github.com/caraml-dev/merlin/assets/8122852/91ac4241-8bf4-401b-983d-99fc7902445b"> 5. On Job Details page: a. Simplify the UI and make it similar and consistent to Version Details page b. Logs is displayed as part of tab navigation c. On Source Config, user can search the feature name <img width="1024" alt="Screenshot 2024-06-12 at 09 39 03" src="https://github.com/caraml-dev/merlin/assets/8122852/15afe282-3942-438a-9e23-52c505b580ee"> # Tests <!-- Besides the existing / updated automated tests, what specific scenarios should be tested? Consider the backward compatibility of the changes, whether corner cases are covered, etc. Please describe the tests and check the ones that have been completed. Eg: - [x] Deploying new and existing standard models - [ ] Deploying PyFunc models --> # Checklist - [x] Added PR label - [ ] Added unit test, integration, and/or e2e tests - [x] Tested locally - [ ] Updated documentation - [ ] Update Swagger spec if the PR introduce API changes - [ ] Regenerated Golang and Python client if the PR introduces API changes # Release Notes <!-- Does this PR introduce a user-facing change? If no, just write "NONE" in the release-note block below. If yes, a release note is required. Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required". For more information about release notes, see kubernetes' guide here: http://git.k8s.io/community/contributors/guide/release-notes.md --> ```release-note Improve batch prediction job UI/UX ```
1 parent cadea49 commit 8edef22

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+5995
-5919
lines changed

.github/workflows/merlin.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ jobs:
222222
- uses: actions/checkout@v4
223223
- uses: actions/setup-node@v4
224224
with:
225-
node-version: 16
225+
node-version: 20
226226
cache: yarn
227227
cache-dependency-path: ui/yarn.lock
228228
- name: Install dependencies

python/sdk/merlin/client.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,9 @@
1616
from sys import version_info
1717
from typing import Any, Dict, List, Optional
1818

19+
import client
1920
import urllib3
2021
from caraml_auth.id_token_credentials import get_default_id_token_credentials
21-
from google.auth.transport.requests import Request
22-
from google.auth.transport.urllib3 import AuthorizedHttp
23-
24-
import client
2522
from client import (
2623
ApiClient,
2724
Configuration,
@@ -33,6 +30,8 @@
3330
StandardTransformerSimulationRequest,
3431
VersionApi,
3532
)
33+
from google.auth.transport.requests import Request
34+
from google.auth.transport.urllib3 import AuthorizedHttp
3635
from merlin.autoscaling import AutoscalingPolicy
3736
from merlin.deployment_mode import DeploymentMode
3837
from merlin.endpoint import VersionEndpoint
@@ -61,7 +60,7 @@ def __init__(self, merlin_url: str, use_google_oauth: bool = True):
6160
# See: https://github.com/googleapis/google-auth-library-python/issues/1211
6261
credentials.refresh(Request())
6362
authorized_http = AuthorizedHttp(credentials, urllib3.PoolManager())
64-
self._api_client.rest_client.pool_manager = authorized_http
63+
self._api_client.rest_client.pool_manager = authorized_http # type: ignore
6564

6665
python_version = f"{version_info.major}.{version_info.minor}.{version_info.micro}" # capture user's python version
6766
self._api_client.user_agent = f"merlin-sdk/{VERSION} python/{python_version}"

ui/package.json

Lines changed: 30 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,53 +4,38 @@
44
"private": true,
55
"homepage": "/merlin",
66
"dependencies": {
7-
"@babel/core": "^7.0.0",
8-
"@babel/plugin-syntax-flow": "^7.14.5",
9-
"@babel/plugin-transform-react-jsx": "^7.14.9",
10-
"@babel/runtime": "7.19.0",
11-
"@caraml-dev/ui-lib": "^1.7.5-build.5-59f13e1",
12-
"@elastic/datemath": "5.0.3",
13-
"@elastic/eui": "64.0.0",
14-
"@emotion/cache": "11.10.3",
15-
"@emotion/react": "^11.9.0",
16-
"@monaco-editor/react": "4.4.5",
17-
"@sentry/browser": "5.15.5",
18-
"@types/react": "^17.0.0",
7+
"@caraml-dev/ui-lib": "^1.12.1-build.15-4f7955c",
8+
"@elastic/datemath": "^5.0.3",
9+
"@elastic/eui": "^94.5.2",
10+
"@emotion/css": "^11.11.2",
11+
"@emotion/react": "^11.11.4",
12+
"@monaco-editor/react": "^4.6.0",
13+
"@sentry/browser": "^8.7.0",
1914
"dagre": "^0.8.5",
2015
"dagre-d3-react": "^0.2.4",
21-
"eslint": "^8.1.0",
2216
"js-yaml": "^4.1.0",
23-
"levenary": "1.1.1",
24-
"moment": "2.29.4",
25-
"monaco-editor": "0.34.0",
26-
"node-sass": "^7.0.3",
27-
"object-assign-deep": "0.4.0",
17+
"moment": "^2.30.1",
18+
"object-assign-deep": "^0.4.0",
2819
"proper-url-join": "^2.1.1",
29-
"react": "^17.0.2",
30-
"react-collapsed": "^3.0.1",
31-
"react-dom": "^17.0.2",
20+
"react": "^18.3.1",
21+
"react-collapsed": "^4.1.2",
22+
"react-dom": "^18.3.1",
3223
"react-ellipsis-text": "^1.2.1",
33-
"react-flow-renderer": "10.3.17",
34-
"react-lazylog": "git+https://github.com/gojekfarm/react-lazylog.git#e3a7f026983df0dc59d25843fe87ce7e37e24e82",
35-
"react-router-dom": "^6.3.0",
36-
"react-scripts": "^5.0.1",
37-
"use-query-params": "^2.1.0",
38-
"yup": "^0.29.1"
24+
"react-flow-renderer": "^10.3.17",
25+
"react-lazylog": "^4.5.3",
26+
"react-router-dom": "^6.23.1",
27+
"use-query-params": "^2.2.1",
28+
"yup": "^1.4.0"
3929
},
4030
"devDependencies": {
41-
"@types/react-dom": "^17.0.0",
42-
"eslint-plugin-flowtype": "^8.0.3",
43-
"husky": "^8.0.1",
44-
"lint-staged": "^13.0.3",
45-
"prettier": "^2.7.1",
31+
"eslint": "^9.3.0",
32+
"eslint-config-react-app": "^7.0.1",
33+
"husky": "^9.0.11",
34+
"lint-staged": "^15.2.5",
35+
"prettier": "^3.2.5",
4636
"prop-types": "^15.8.1",
47-
"typescript": "4.5.3"
48-
},
49-
"lint-staged": {
50-
"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}": [
51-
"prettier --jsx-bracket-same-line --write",
52-
"git add"
53-
]
37+
"react-scripts": "^5.0.1",
38+
"sass": "^1.77.2"
5439
},
5540
"scripts": {
5641
"start": "react-scripts start",
@@ -65,6 +50,12 @@
6550
"eslintConfig": {
6651
"extends": "react-app"
6752
},
53+
"lint-staged": {
54+
"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}": [
55+
"prettier --jsx-bracket-same-line --write",
56+
"git add"
57+
]
58+
},
6859
"husky": {
6960
"hooks": {
7061
"pre-commit": "lint-staged"

ui/src/AppRoutes.js

Lines changed: 56 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
import React from "react";
22
import { Navigate, Route, Routes } from "react-router-dom";
3-
4-
import config from "./config";
53
import Home from "./Home";
6-
import Models from "./model/Models";
4+
import config from "./config";
75
import { ModelDetails } from "./model/ModelDetails";
6+
import Models from "./model/Models";
87
import Versions from "./version/Versions";
9-
import { CreateJobView } from "./job/CreateJobView";
10-
import JobDetails from "./job/JobDetails";
11-
import Jobs from "./job/Jobs";
128

139
// The new UI architecture will have all UI pages inside of `pages` folder
1410
import {
11+
CreateJobPage,
1512
DeployModelVersionView,
13+
JobPage,
14+
ListJobsPage,
15+
RecreateJobPage,
1616
RedeployModelVersionView,
1717
TransformerTools,
1818
VersionDetails,
@@ -34,22 +34,61 @@ const AppRoutes = () => {
3434
<Route path=":modelId/*" element={<ModelDetails />} />
3535
{/* VERSIONS */}
3636
<Route path=":modelId/versions/*" element={<Versions />} />
37-
<Route path=":modelId/versions/:versionId/*" element={<VersionDetails />} />
38-
<Route path=":modelId/versions/:versionId/deploy" element={<DeployModelVersionView />} />
37+
<Route
38+
path=":modelId/versions/:versionId/*"
39+
element={<VersionDetails />}
40+
/>
41+
<Route
42+
path=":modelId/versions/:versionId/deploy"
43+
element={<DeployModelVersionView />}
44+
/>
3945
{/* VERSIONS ENDPOINTS */}
4046
<Route path=":modelId/versions/:versionId/endpoints">
41-
<Route index={true} path=":endpointId/*" element={<VersionDetails />} />
42-
<Route path=":endpointId/redeploy" element={<RedeployModelVersionView />} />
47+
<Route
48+
index={true}
49+
path=":endpointId/*"
50+
element={<VersionDetails />}
51+
/>
52+
<Route
53+
path=":endpointId/redeploy"
54+
element={<RedeployModelVersionView />}
55+
/>
4356
</Route>
4457
{/* BATCH JOBS */}
45-
<Route path=":modelId/versions/:versionId/jobs" element={<Jobs />} />
46-
<Route path=":modelId/versions/:versionId/jobs/:jobId/*" element={<JobDetails />} />
47-
<Route path=":modelId/create-job" element={<CreateJobView />} />
48-
<Route path=":modelId/versions/:versionId/create-job" element={<CreateJobView />} />
58+
<Route
59+
path=":modelId/versions/:versionId/jobs"
60+
element={<ListJobsPage />}
61+
/>
62+
<Route
63+
path=":modelId/versions/:versionId/jobs/:jobId/*"
64+
element={<JobPage />}
65+
/>
66+
<Route
67+
path=":modelId/versions/:versionId/jobs/:jobId/recreate"
68+
element={<RecreateJobPage />}
69+
/>
70+
<Route path=":modelId/create-job" element={<CreateJobPage />} />
71+
<Route
72+
path=":modelId/versions/:versionId/create-job"
73+
element={<CreateJobPage />}
74+
/>
75+
<Route
76+
path=":modelId/versions/:versionId/recreate"
77+
element={<CreateJobPage />}
78+
/>
4979
{/* REDIRECTS */}
50-
<Route path=":modelId" element={<Navigate to="versions" replace={true} />} />
51-
<Route path=":modelId/versions/:versionId" element={<Navigate to="details" replace={true} />} />
52-
<Route path=":modelId/versions/:versionId/endpoints/:endpointId" element={<Navigate to="details" replace={true} />} />
80+
<Route
81+
path=":modelId"
82+
element={<Navigate to="versions" replace={true} />}
83+
/>
84+
<Route
85+
path=":modelId/versions/:versionId"
86+
element={<Navigate to="details" replace={true} />}
87+
/>
88+
<Route
89+
path=":modelId/versions/:versionId/endpoints/:endpointId"
90+
element={<Navigate to="details" replace={true} />}
91+
/>
5392
</Route>
5493
</Route>
5594
</Route>

ui/src/bootstrap.js

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,34 +14,32 @@
1414
* limitations under the License.
1515
*/
1616

17+
import * as Sentry from "@sentry/browser";
1718
import React from "react";
18-
import ReactDOM from "react-dom";
19+
import { createRoot } from "react-dom/client";
20+
import { BrowserRouter } from "react-router-dom";
1921
import App from "./App";
20-
import * as Sentry from "@sentry/browser";
21-
import * as serviceWorker from "./serviceWorker";
2222
import { ConfigProvider, sentryConfig } from "./config";
23-
import { BrowserRouter } from "react-router-dom";
23+
import * as serviceWorker from "./serviceWorker";
2424

2525
require("./assets/scss/index.scss");
2626

27-
const MerlinUI = () => (
28-
<React.StrictMode>
29-
<ConfigProvider>
30-
<BrowserRouter>
31-
<App />
32-
</BrowserRouter>
33-
</ConfigProvider>
34-
</React.StrictMode>
35-
);
36-
3727
if (sentryConfig.dsn !== "") {
38-
Sentry.init(sentryConfig);
28+
Sentry.init(sentryConfig);
3929
}
40-
41-
ReactDOM.render(MerlinUI(), document.getElementById("root"));
42-
30+
31+
const container = document.getElementById("root");
32+
const root = createRoot(container);
33+
34+
root.render(
35+
<ConfigProvider>
36+
<BrowserRouter>
37+
<App />
38+
</BrowserRouter>
39+
</ConfigProvider>,
40+
);
41+
4342
// If you want your app to work offline and load faster, you can change
4443
// unregister() to register() below. Note this comes with some pitfalls.
4544
// Learn more about service workers: https://bit.ly/CRA-PWA
4645
serviceWorker.unregister();
47-

ui/src/components/CopyableUrl.js

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,21 @@
1414
* limitations under the License.
1515
*/
1616

17-
import React from "react";
18-
import PropTypes from "prop-types";
1917
import { EuiCopy, EuiIcon, EuiLink, EuiText } from "@elastic/eui";
18+
import PropTypes from "prop-types";
19+
import React from "react";
2020

2121
export const CopyableUrl = ({ text, iconSize }) => {
2222
return text ? (
2323
<EuiCopy textToCopy={text} beforeMessage="Click to copy URL to clipboard">
24-
{copy => (
24+
{(copy) => (
2525
<EuiLink
26-
onClick={e => {
26+
onClick={(e) => {
2727
e.stopPropagation();
2828
copy();
2929
}}
30-
color="text">
30+
color="text"
31+
>
3132
<EuiText size="s">
3233
<EuiIcon
3334
type={"copyClipboard"}
@@ -46,5 +47,5 @@ export const CopyableUrl = ({ text, iconSize }) => {
4647

4748
CopyableUrl.propTypes = {
4849
text: PropTypes.string.isRequired,
49-
iconSize: PropTypes.oneOf(["xs", "s", "m", "l", "xl"])
50+
iconSize: PropTypes.oneOf(["xs", "s", "m", "l", "xl"]),
5051
};

ui/src/components/ResourcesConfigTable.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,9 @@ export const ResourcesConfigTable = ({
7171
return (
7272
<EuiDescriptionList
7373
compressed
74-
textStyle="reverse"
7574
type="responsiveColumn"
7675
listItems={items}
76+
columnWidths={[1, 1]}
7777
/>
7878
);
7979
};

ui/src/components/TabNavigation.js

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
* limitations under the License.
1515
*/
1616

17-
import React, { useState } from "react";
1817
import {
1918
EuiContextMenuItem,
2019
EuiContextMenuPanel,
@@ -23,17 +22,16 @@ import {
2322
EuiIcon,
2423
EuiPopover,
2524
EuiTab,
26-
EuiTabs
25+
EuiTabs,
2726
} from "@elastic/eui";
28-
29-
import "./TabNavigation.scss";
27+
import React, { useState } from "react";
3028

3129
const MoreActionsButton = ({ actions }) => {
3230
const [isPopoverOpen, setPopover] = useState(false);
33-
const togglePopover = () => setPopover(isPopoverOpen => !isPopoverOpen);
31+
const togglePopover = () => setPopover((isPopoverOpen) => !isPopoverOpen);
3432

3533
const items = actions
36-
.filter(item => !item.hidden)
34+
.filter((item) => !item.hidden)
3735
.map((item, idx) => (
3836
<EuiContextMenuItem
3937
key={idx}
@@ -43,7 +41,8 @@ const MoreActionsButton = ({ actions }) => {
4341
item.onClick();
4442
}}
4543
disabled={item.disabled}
46-
className={item.color ? `euiTextColor--${item.color}` : ""}>
44+
className={item.color ? `euiTextColor--${item.color}` : ""}
45+
>
4746
{item.name}
4847
</EuiContextMenuItem>
4948
));
@@ -69,12 +68,9 @@ const MoreActionsButton = ({ actions }) => {
6968
isOpen={isPopoverOpen}
7069
closePopover={togglePopover}
7170
panelPaddingSize="none"
72-
anchorPosition="downRight">
73-
<EuiContextMenuPanel
74-
hasFocus={false}
75-
className="euiContextPanel--moreActions"
76-
items={items}
77-
/>
71+
anchorPosition="downRight"
72+
>
73+
<EuiContextMenuPanel hasFocus={false} items={items} />
7874
</EuiPopover>
7975
);
8076
};
@@ -90,7 +86,8 @@ export const TabNavigation = ({ tabs, actions, selectedTab, navigate }) => (
9086
: { onClick: () => navigate(`./${tab.id}`) })}
9187
isSelected={tab.id === selectedTab}
9288
disabled={tab.disabled}
93-
key={index}>
89+
key={index}
90+
>
9491
{tab.name}
9592
</EuiTab>
9693
))}

0 commit comments

Comments
 (0)