Skip to content

Commit

Permalink
chore: Revamp Batch Prediction Jobs UI (#591)
Browse files Browse the repository at this point in the history
<!--  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
```
  • Loading branch information
ariefrahmansyah authored Jun 21, 2024
1 parent cadea49 commit 8edef22
Show file tree
Hide file tree
Showing 64 changed files with 5,995 additions and 5,919 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/merlin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 16
node-version: 20
cache: yarn
cache-dependency-path: ui/yarn.lock
- name: Install dependencies
Expand Down
9 changes: 4 additions & 5 deletions python/sdk/merlin/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,9 @@
from sys import version_info
from typing import Any, Dict, List, Optional

import client
import urllib3
from caraml_auth.id_token_credentials import get_default_id_token_credentials
from google.auth.transport.requests import Request
from google.auth.transport.urllib3 import AuthorizedHttp

import client
from client import (
ApiClient,
Configuration,
Expand All @@ -33,6 +30,8 @@
StandardTransformerSimulationRequest,
VersionApi,
)
from google.auth.transport.requests import Request
from google.auth.transport.urllib3 import AuthorizedHttp
from merlin.autoscaling import AutoscalingPolicy
from merlin.deployment_mode import DeploymentMode
from merlin.endpoint import VersionEndpoint
Expand Down Expand Up @@ -61,7 +60,7 @@ def __init__(self, merlin_url: str, use_google_oauth: bool = True):
# See: https://github.com/googleapis/google-auth-library-python/issues/1211
credentials.refresh(Request())
authorized_http = AuthorizedHttp(credentials, urllib3.PoolManager())
self._api_client.rest_client.pool_manager = authorized_http
self._api_client.rest_client.pool_manager = authorized_http # type: ignore

python_version = f"{version_info.major}.{version_info.minor}.{version_info.micro}" # capture user's python version
self._api_client.user_agent = f"merlin-sdk/{VERSION} python/{python_version}"
Expand Down
69 changes: 30 additions & 39 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,53 +4,38 @@
"private": true,
"homepage": "/merlin",
"dependencies": {
"@babel/core": "^7.0.0",
"@babel/plugin-syntax-flow": "^7.14.5",
"@babel/plugin-transform-react-jsx": "^7.14.9",
"@babel/runtime": "7.19.0",
"@caraml-dev/ui-lib": "^1.7.5-build.5-59f13e1",
"@elastic/datemath": "5.0.3",
"@elastic/eui": "64.0.0",
"@emotion/cache": "11.10.3",
"@emotion/react": "^11.9.0",
"@monaco-editor/react": "4.4.5",
"@sentry/browser": "5.15.5",
"@types/react": "^17.0.0",
"@caraml-dev/ui-lib": "^1.12.1-build.15-4f7955c",
"@elastic/datemath": "^5.0.3",
"@elastic/eui": "^94.5.2",
"@emotion/css": "^11.11.2",
"@emotion/react": "^11.11.4",
"@monaco-editor/react": "^4.6.0",
"@sentry/browser": "^8.7.0",
"dagre": "^0.8.5",
"dagre-d3-react": "^0.2.4",
"eslint": "^8.1.0",
"js-yaml": "^4.1.0",
"levenary": "1.1.1",
"moment": "2.29.4",
"monaco-editor": "0.34.0",
"node-sass": "^7.0.3",
"object-assign-deep": "0.4.0",
"moment": "^2.30.1",
"object-assign-deep": "^0.4.0",
"proper-url-join": "^2.1.1",
"react": "^17.0.2",
"react-collapsed": "^3.0.1",
"react-dom": "^17.0.2",
"react": "^18.3.1",
"react-collapsed": "^4.1.2",
"react-dom": "^18.3.1",
"react-ellipsis-text": "^1.2.1",
"react-flow-renderer": "10.3.17",
"react-lazylog": "git+https://github.com/gojekfarm/react-lazylog.git#e3a7f026983df0dc59d25843fe87ce7e37e24e82",
"react-router-dom": "^6.3.0",
"react-scripts": "^5.0.1",
"use-query-params": "^2.1.0",
"yup": "^0.29.1"
"react-flow-renderer": "^10.3.17",
"react-lazylog": "^4.5.3",
"react-router-dom": "^6.23.1",
"use-query-params": "^2.2.1",
"yup": "^1.4.0"
},
"devDependencies": {
"@types/react-dom": "^17.0.0",
"eslint-plugin-flowtype": "^8.0.3",
"husky": "^8.0.1",
"lint-staged": "^13.0.3",
"prettier": "^2.7.1",
"eslint": "^9.3.0",
"eslint-config-react-app": "^7.0.1",
"husky": "^9.0.11",
"lint-staged": "^15.2.5",
"prettier": "^3.2.5",
"prop-types": "^15.8.1",
"typescript": "4.5.3"
},
"lint-staged": {
"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}": [
"prettier --jsx-bracket-same-line --write",
"git add"
]
"react-scripts": "^5.0.1",
"sass": "^1.77.2"
},
"scripts": {
"start": "react-scripts start",
Expand All @@ -65,6 +50,12 @@
"eslintConfig": {
"extends": "react-app"
},
"lint-staged": {
"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}": [
"prettier --jsx-bracket-same-line --write",
"git add"
]
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
Expand Down
73 changes: 56 additions & 17 deletions ui/src/AppRoutes.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import React from "react";
import { Navigate, Route, Routes } from "react-router-dom";

import config from "./config";
import Home from "./Home";
import Models from "./model/Models";
import config from "./config";
import { ModelDetails } from "./model/ModelDetails";
import Models from "./model/Models";
import Versions from "./version/Versions";
import { CreateJobView } from "./job/CreateJobView";
import JobDetails from "./job/JobDetails";
import Jobs from "./job/Jobs";

// The new UI architecture will have all UI pages inside of `pages` folder
import {
CreateJobPage,
DeployModelVersionView,
JobPage,
ListJobsPage,
RecreateJobPage,
RedeployModelVersionView,
TransformerTools,
VersionDetails,
Expand All @@ -34,22 +34,61 @@ const AppRoutes = () => {
<Route path=":modelId/*" element={<ModelDetails />} />
{/* VERSIONS */}
<Route path=":modelId/versions/*" element={<Versions />} />
<Route path=":modelId/versions/:versionId/*" element={<VersionDetails />} />
<Route path=":modelId/versions/:versionId/deploy" element={<DeployModelVersionView />} />
<Route
path=":modelId/versions/:versionId/*"
element={<VersionDetails />}
/>
<Route
path=":modelId/versions/:versionId/deploy"
element={<DeployModelVersionView />}
/>
{/* VERSIONS ENDPOINTS */}
<Route path=":modelId/versions/:versionId/endpoints">
<Route index={true} path=":endpointId/*" element={<VersionDetails />} />
<Route path=":endpointId/redeploy" element={<RedeployModelVersionView />} />
<Route
index={true}
path=":endpointId/*"
element={<VersionDetails />}
/>
<Route
path=":endpointId/redeploy"
element={<RedeployModelVersionView />}
/>
</Route>
{/* BATCH JOBS */}
<Route path=":modelId/versions/:versionId/jobs" element={<Jobs />} />
<Route path=":modelId/versions/:versionId/jobs/:jobId/*" element={<JobDetails />} />
<Route path=":modelId/create-job" element={<CreateJobView />} />
<Route path=":modelId/versions/:versionId/create-job" element={<CreateJobView />} />
<Route
path=":modelId/versions/:versionId/jobs"
element={<ListJobsPage />}
/>
<Route
path=":modelId/versions/:versionId/jobs/:jobId/*"
element={<JobPage />}
/>
<Route
path=":modelId/versions/:versionId/jobs/:jobId/recreate"
element={<RecreateJobPage />}
/>
<Route path=":modelId/create-job" element={<CreateJobPage />} />
<Route
path=":modelId/versions/:versionId/create-job"
element={<CreateJobPage />}
/>
<Route
path=":modelId/versions/:versionId/recreate"
element={<CreateJobPage />}
/>
{/* REDIRECTS */}
<Route path=":modelId" element={<Navigate to="versions" replace={true} />} />
<Route path=":modelId/versions/:versionId" element={<Navigate to="details" replace={true} />} />
<Route path=":modelId/versions/:versionId/endpoints/:endpointId" element={<Navigate to="details" replace={true} />} />
<Route
path=":modelId"
element={<Navigate to="versions" replace={true} />}
/>
<Route
path=":modelId/versions/:versionId"
element={<Navigate to="details" replace={true} />}
/>
<Route
path=":modelId/versions/:versionId/endpoints/:endpointId"
element={<Navigate to="details" replace={true} />}
/>
</Route>
</Route>
</Route>
Expand Down
36 changes: 17 additions & 19 deletions ui/src/bootstrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,34 +14,32 @@
* limitations under the License.
*/

import * as Sentry from "@sentry/browser";
import React from "react";
import ReactDOM from "react-dom";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./App";
import * as Sentry from "@sentry/browser";
import * as serviceWorker from "./serviceWorker";
import { ConfigProvider, sentryConfig } from "./config";
import { BrowserRouter } from "react-router-dom";
import * as serviceWorker from "./serviceWorker";

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

const MerlinUI = () => (
<React.StrictMode>
<ConfigProvider>
<BrowserRouter>
<App />
</BrowserRouter>
</ConfigProvider>
</React.StrictMode>
);

if (sentryConfig.dsn !== "") {
Sentry.init(sentryConfig);
Sentry.init(sentryConfig);
}

ReactDOM.render(MerlinUI(), document.getElementById("root"));


const container = document.getElementById("root");
const root = createRoot(container);

root.render(
<ConfigProvider>
<BrowserRouter>
<App />
</BrowserRouter>
</ConfigProvider>,
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

13 changes: 7 additions & 6 deletions ui/src/components/CopyableUrl.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,21 @@
* limitations under the License.
*/

import React from "react";
import PropTypes from "prop-types";
import { EuiCopy, EuiIcon, EuiLink, EuiText } from "@elastic/eui";
import PropTypes from "prop-types";
import React from "react";

export const CopyableUrl = ({ text, iconSize }) => {
return text ? (
<EuiCopy textToCopy={text} beforeMessage="Click to copy URL to clipboard">
{copy => (
{(copy) => (
<EuiLink
onClick={e => {
onClick={(e) => {
e.stopPropagation();
copy();
}}
color="text">
color="text"
>
<EuiText size="s">
<EuiIcon
type={"copyClipboard"}
Expand All @@ -46,5 +47,5 @@ export const CopyableUrl = ({ text, iconSize }) => {

CopyableUrl.propTypes = {
text: PropTypes.string.isRequired,
iconSize: PropTypes.oneOf(["xs", "s", "m", "l", "xl"])
iconSize: PropTypes.oneOf(["xs", "s", "m", "l", "xl"]),
};
2 changes: 1 addition & 1 deletion ui/src/components/ResourcesConfigTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@ export const ResourcesConfigTable = ({
return (
<EuiDescriptionList
compressed
textStyle="reverse"
type="responsiveColumn"
listItems={items}
columnWidths={[1, 1]}
/>
);
};
Expand Down
25 changes: 11 additions & 14 deletions ui/src/components/TabNavigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
* limitations under the License.
*/

import React, { useState } from "react";
import {
EuiContextMenuItem,
EuiContextMenuPanel,
Expand All @@ -23,17 +22,16 @@ import {
EuiIcon,
EuiPopover,
EuiTab,
EuiTabs
EuiTabs,
} from "@elastic/eui";

import "./TabNavigation.scss";
import React, { useState } from "react";

const MoreActionsButton = ({ actions }) => {
const [isPopoverOpen, setPopover] = useState(false);
const togglePopover = () => setPopover(isPopoverOpen => !isPopoverOpen);
const togglePopover = () => setPopover((isPopoverOpen) => !isPopoverOpen);

const items = actions
.filter(item => !item.hidden)
.filter((item) => !item.hidden)
.map((item, idx) => (
<EuiContextMenuItem
key={idx}
Expand All @@ -43,7 +41,8 @@ const MoreActionsButton = ({ actions }) => {
item.onClick();
}}
disabled={item.disabled}
className={item.color ? `euiTextColor--${item.color}` : ""}>
className={item.color ? `euiTextColor--${item.color}` : ""}
>
{item.name}
</EuiContextMenuItem>
));
Expand All @@ -69,12 +68,9 @@ const MoreActionsButton = ({ actions }) => {
isOpen={isPopoverOpen}
closePopover={togglePopover}
panelPaddingSize="none"
anchorPosition="downRight">
<EuiContextMenuPanel
hasFocus={false}
className="euiContextPanel--moreActions"
items={items}
/>
anchorPosition="downRight"
>
<EuiContextMenuPanel hasFocus={false} items={items} />
</EuiPopover>
);
};
Expand All @@ -90,7 +86,8 @@ export const TabNavigation = ({ tabs, actions, selectedTab, navigate }) => (
: { onClick: () => navigate(`./${tab.id}`) })}
isSelected={tab.id === selectedTab}
disabled={tab.disabled}
key={index}>
key={index}
>
{tab.name}
</EuiTab>
))}
Expand Down
Loading

0 comments on commit 8edef22

Please sign in to comment.