Skip to content

Commit

Permalink
Update App Launcher Create App Form based on new design (#146)
Browse files Browse the repository at this point in the history
* Add create and edit app pages. Update routing for pages on separate base.

* Add app form to create and edit app pages. Update styling. Update theme.

* Add view thumbnail action to thumbnail component. Add icons to buttons.

* Add conda_env to dummy app data.

* Update form to replace custom formgroup with mui formcontrol.

* Fix unit tests.

* Misc form updates to match designs.

* Add back button to app form pages.

* Update server types page styling.

* Updates to submit on server types page. Misc cleanup.

* Add routes and page template for new pages. Misc styling updates.

* Add and update unit tests.

* Add new build.

* Update to add server type click event to card.

* Update to provide app creation through main form when new server types are available.

* Update thumbnail component styling to match mockup.

* Update to persist form data on back. Updates to server type styling.

* Update jhub styling.

* Fix integration test.

* Add and update unit tests. Misc cleanup.

* Fix issue loading env variables into edit form.

* Fix lint issue.

* Add unit tests. Unit test cleanup.

* Update jinja template path.

* Fix env field when clicking back.

* Update template_path import.
  • Loading branch information
jbouder committed Mar 6, 2024
1 parent 9d37c2f commit 6a5df08
Show file tree
Hide file tree
Showing 33 changed files with 1,564 additions and 818 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -293,3 +293,4 @@ cython_debug/
jupyterhub.sqlite
jupyterhub_cookie_secret
.DS_Store
videos/
2 changes: 2 additions & 0 deletions jhub_apps/service/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles

from jhub_apps.service.japps_routes import router as japps_router
from jhub_apps.service.logging_utils import setup_logging
from jhub_apps.service.middlewares import create_middlewares
from jhub_apps.service.routes import router
Expand Down Expand Up @@ -33,5 +34,6 @@
static_files = StaticFiles(directory=STATIC_DIR)
app.mount(f"{router.prefix}/static", static_files, name="static")
app.include_router(router)
app.include_router(japps_router)
create_middlewares(app)
setup_logging()
17 changes: 17 additions & 0 deletions jhub_apps/service/japps_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from fastapi import APIRouter, FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates

from jhub_apps import TEMPLATE_PATH

app = FastAPI()

templates = Jinja2Templates(directory=TEMPLATE_PATH)
router = APIRouter(prefix="/services/japps")


@router.get("/create-app", response_class=HTMLResponse)
@router.get("/edit-app", response_class=HTMLResponse)
@router.get("/server-types", response_class=HTMLResponse)
async def handle_apps(request: Request):
return templates.TemplateResponse("japps_home.html", {"request": request})
2 changes: 1 addition & 1 deletion jhub_apps/static/css/index.css

Large diffs are not rendered by default.

65 changes: 37 additions & 28 deletions jhub_apps/static/js/index.js

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions jhub_apps/templates/japps_home.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<html>
<head>
<link rel="stylesheet" href="/hub/static/css/style.min.css" />
</head>
<body>
<div id="root"></div>
<script src="/services/japps/static/js/index.js?v={{version_hash}}"></script>
<link
rel="stylesheet"
href="/services/japps/static/css/index.css?v={{version_hash}}"
/>
<style>
{% include 'style.css' %}
</style>
</body>
</html>
20 changes: 18 additions & 2 deletions jhub_apps/templates/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,11 @@ a:focus {
padding: 0 !important;
}

.row.breadcrumb {
background-color: unset !important;
padding: 0 !important;
}

/* Buttons */
.btn {
font-family: var(--base-font-family) !important;
Expand Down Expand Up @@ -181,6 +186,10 @@ a:focus {
border-color: var(--gray-color) !important;
}

.MuiButton-containedSecondary {
background-color: var(--gray-color) !important;
}

.btn-secondary, .MuiButton-outlinedSecondary:not(:disabled) {
color: var(--secondary-color) !important;
background-color: #ffffff !important;
Expand All @@ -197,6 +206,10 @@ a:focus {
outline-color: var(--secondary-color) !important;
}

.MuiButton-textPrimary {
color: var(--primary-color) !important;
}

.btn-success {
color: var(--text-color);
background-color: var(--secondary-color) !important;
Expand Down Expand Up @@ -244,6 +257,11 @@ a:focus {
margin-left: 8px;
}

/* Misc MUI */
.MuiCardContent-root:focus {
outline-color: var(--primary-color) !important;
}

/* Navbar */
.nav {
margin-left:10px;
Expand Down Expand Up @@ -372,8 +390,6 @@ a:focus {
border-top: 1px solid #e6e6e6;
}



@media only screen and (max-width: 480px) {
.navbar-nav {
margin: 0;
Expand Down
13 changes: 7 additions & 6 deletions jhub_apps/tests_e2e/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def get_page(playwright: Playwright):
record_video_dir="videos/",
record_video_size={"width": 1920, "height": 1080},
viewport={"width": 1920, "height": 1080},
ignore_https_errors=True
ignore_https_errors=True,
)
page = context.new_page()
return browser, context, page
Expand All @@ -36,7 +36,7 @@ def test_panel_app_creation(playwright: Playwright) -> None:
# for searching app with unique name in the UI
app_name = f"{framework} app {app_suffix}"
app_page_title = "Panel Test App"
wait_for_element_in_app = 'div.bk-slider-title >> text=Slider:'
wait_for_element_in_app = "div.bk-slider-title >> text=Slider:"
try:
page.goto(BASE_URL)
logger.info("Signing in")
Expand All @@ -50,12 +50,13 @@ def test_panel_app_creation(playwright: Playwright) -> None:
logger.info("Creating App")
page.get_by_role("button", name="Create App").click()
logger.info("Fill App display Name")
page.get_by_label("Display Name *").click()
page.get_by_label("Display Name *").fill(app_name)
page.get_by_label("Name *").click()
page.get_by_label("Name *").fill(app_name)
logger.info("Select Framework")
page.get_by_label("Framework *").select_option(framework)
page.locator("id=framework").click()
page.get_by_role("option", name="Panel").click()
logger.info("Click Submit")
page.get_by_role("button", name="Submit").click()
page.get_by_role("button", name="Create App").click()
slider_text_element = page.wait_for_selector(wait_for_element_in_app)
assert slider_text_element is not None, "Slider text element not found!"
logger.info("Checking page title")
Expand Down
17 changes: 8 additions & 9 deletions ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,21 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import React, { useEffect } from 'react';
import { Route, Routes } from 'react-router';
import { useRecoilState } from 'recoil';
import { CreateApp } from './pages/create-app/create-app';
import { EditApp } from './pages/edit-app/edit-app';
import { Home } from './pages/home/home';
import { ServerTypes } from './pages/server-types/server-types';
import { currentJhData } from './store';
import { JhData } from './types/jupyterhub';
import { getJhData } from './utils/jupyterhub';
import { getJhData, storeJhData } from './utils/jupyterhub';

const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
},
},
});
const queryClient = new QueryClient();

export const App = (): React.ReactElement => {
const [, setJhData] = useRecoilState<JhData>(currentJhData);
useEffect(() => {
setJhData(getJhData());
storeJhData(getJhData());
}, [setJhData]);

return (
Expand All @@ -28,8 +25,10 @@ export const App = (): React.ReactElement => {
<main className="my-6">
<Routes>
<Route path="/home" element={<Home />} />
<Route path="/" element={<Home />} />
<Route path="/create-app" element={<CreateApp />} />
<Route path="/edit-app" element={<EditApp />} />
<Route path="/server-types" element={<ServerTypes />} />
<Route path="/" element={<Home />} />
</Routes>
</main>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { act, fireEvent, render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import MockAdapter from 'axios-mock-adapter';
import { BrowserRouter } from 'react-router-dom';
import { RecoilRoot } from 'recoil';
import AppForm from './app-form';

Expand All @@ -28,7 +29,9 @@ describe('AppForm', () => {
const { baseElement } = render(
<RecoilRoot>
<QueryClientProvider client={queryClient}>
<AppForm />
<BrowserRouter>
<AppForm />
</BrowserRouter>
</QueryClientProvider>
</RecoilRoot>,
);
Expand All @@ -44,7 +47,9 @@ describe('AppForm', () => {
const { baseElement } = render(
<RecoilRoot>
<QueryClientProvider client={queryClient}>
<AppForm />
<BrowserRouter>
<AppForm />
</BrowserRouter>
</QueryClientProvider>
</RecoilRoot>,
);
Expand All @@ -53,7 +58,7 @@ describe('AppForm', () => {
'#display_name',
) as HTMLInputElement;
const frameworkField = baseElement.querySelector(
'#framework',
'[name="framework"]',
) as HTMLSelectElement;
const envVariableField = baseElement.querySelector(
'#env',
Expand Down Expand Up @@ -88,7 +93,9 @@ describe('AppForm', () => {
const { baseElement } = render(
<RecoilRoot>
<QueryClientProvider client={queryClient}>
<AppForm />
<BrowserRouter>
<AppForm />
</BrowserRouter>
</QueryClientProvider>
</RecoilRoot>,
);
Expand All @@ -100,7 +107,7 @@ describe('AppForm', () => {
'#thumbnail',
) as HTMLInputElement;
const frameworkField = baseElement.querySelector(
'#framework',
'[name="framework"]',
) as HTMLSelectElement;
if (displayNameField && thumbnailField && frameworkField) {
// Attempt submitting without filling in required fields
Expand Down Expand Up @@ -133,7 +140,9 @@ describe('AppForm', () => {
const { baseElement } = render(
<RecoilRoot>
<QueryClientProvider client={queryClient}>
<AppForm onSubmit={jest.fn} />
<BrowserRouter>
<AppForm />
</BrowserRouter>
</QueryClientProvider>
</RecoilRoot>,
);
Expand All @@ -142,7 +151,7 @@ describe('AppForm', () => {
'#display_name',
) as HTMLInputElement;
const frameworkField = baseElement.querySelector(
'#framework',
'[name="framework"]',
) as HTMLSelectElement;
if (displayNameField && frameworkField) {
const btn = baseElement.querySelector('#submit-btn') as HTMLButtonElement;
Expand All @@ -165,7 +174,9 @@ describe('AppForm', () => {
const { baseElement } = render(
<RecoilRoot>
<QueryClientProvider client={queryClient}>
<AppForm />
<BrowserRouter>
<AppForm />
</BrowserRouter>
</QueryClientProvider>
</RecoilRoot>,
);
Expand Down Expand Up @@ -228,7 +239,9 @@ describe('AppForm', () => {
const { baseElement } = render(
<RecoilRoot>
<QueryClientProvider client={queryClient}>
<AppForm />
<BrowserRouter>
<AppForm />
</BrowserRouter>
</QueryClientProvider>
</RecoilRoot>,
);
Expand All @@ -240,7 +253,7 @@ describe('AppForm', () => {
'#description',
) as HTMLInputElement;
const frameworkField = baseElement.querySelector(
'#framework',
'[name="framework"]',
) as HTMLSelectElement;
if (displayNameField && descriptionField && frameworkField) {
await userEvent.type(displayNameField, 'App 1');
Expand All @@ -265,7 +278,9 @@ describe('AppForm', () => {
const { baseElement } = render(
<RecoilRoot>
<QueryClientProvider client={queryClient}>
<AppForm id="app-1" />
<BrowserRouter>
<AppForm id="app-1" />
</BrowserRouter>
</QueryClientProvider>
</RecoilRoot>,
);
Expand All @@ -277,7 +292,7 @@ describe('AppForm', () => {
'#description',
) as HTMLInputElement;
const frameworkField = baseElement.querySelector(
'#framework',
'[name="framework"]',
) as HTMLSelectElement;
if (displayNameField && descriptionField && frameworkField) {
await userEvent.type(displayNameField, 'App 1');
Expand All @@ -300,7 +315,9 @@ describe('AppForm', () => {
const { baseElement } = render(
<RecoilRoot>
<QueryClientProvider client={queryClient}>
<AppForm id="app-1" />
<BrowserRouter>
<AppForm id="app-1" />
</BrowserRouter>
</QueryClientProvider>
</RecoilRoot>,
);
Expand All @@ -312,7 +329,7 @@ describe('AppForm', () => {
'#description',
) as HTMLInputElement;
const frameworkField = baseElement.querySelector(
'#framework',
'[name="framework"]',
) as HTMLSelectElement;
if (displayNameField && descriptionField && frameworkField) {
await userEvent.type(displayNameField, 'App 1');
Expand All @@ -325,4 +342,23 @@ describe('AppForm', () => {
});
}
});

test('clicks cancel to home', async () => {
mock.onGet(new RegExp('/frameworks')).reply(200, frameworks);
mock.onGet(new RegExp('/server/app-1')).reply(200, app);
const { baseElement } = render(
<RecoilRoot>
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<AppForm />
</BrowserRouter>
</QueryClientProvider>
</RecoilRoot>,
);
const btn = baseElement.querySelector('#cancel-btn') as HTMLButtonElement;
await act(async () => {
btn.click();
});
expect(window.location.pathname).toBe('/');
});
});
Loading

0 comments on commit 6a5df08

Please sign in to comment.