Skip to content

Commit

Permalink
Sharing messaging (nebari-dev#481)
Browse files Browse the repository at this point in the history
* updated frontend and backend code to handle 403, 404, 500 errors

* adding new build index.js file

* updated the messaging

* fixed errors in tests

* added logic to cover clicking directly on a card. updated tests, updated copy

* updated tests, removed commented out tests

* fixed linting error

* removed two tests

* added tests for start_server, server_not_found

* updated *Name to Name in test_integration.py test

* updated the condtional for starting app, removed unused imports

* reverted tests in test_api.py, updated logic to allow admins to start/stop apps

* Trigger re-run of tests

* Trigger re-run of tests

* updated failing tests, all tests passing

* removed unnecessary import for pytest

* generate index.js

* undo unrequired changes

---------

Co-authored-by: Kilian Berres <kberres@ITLP01061.local>
Co-authored-by: Amit Kumar <dtu.amit@gmail.com>
  • Loading branch information
3 people authored Oct 1, 2024
1 parent 30fcc5d commit 4d9f3b9
Show file tree
Hide file tree
Showing 8 changed files with 497 additions and 48 deletions.
52 changes: 26 additions & 26 deletions jhub_apps/static/js/index.js

Large diffs are not rendered by default.

118 changes: 118 additions & 0 deletions ui/src/components/app-card/app-card.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -578,4 +578,122 @@ describe('AppCard', () => {
);
expect(getByTestId('LockRoundedIcon')).toBeInTheDocument();
});
test('renders context menu with start, stop, edit, and delete actions', async () => {
const { getByTestId, getByText } = render(
<RecoilRoot>
<QueryClientProvider client={queryClient}>
<AppCard
id="1"
title="Test App"
username="Developer"
framework="Some Framework"
url="/some-url"
serverStatus="Ready"
isShared={false}
app={{ id: '1', name: 'Test App', framework: 'Some Framework', description: 'Test App 1',
url: '/user/test/test-app-1/',
thumbnail: '',
username: 'test',
ready: true,
public: false,
shared: false,
last_activity: new Date(),
pending: false,
stopped: false,
status: 'false' }}
/>
</QueryClientProvider>
</RecoilRoot>,
);

// Open context menu first
const contextMenuButton = getByTestId('context-menu-button-card-menu-1');
act(() => {
contextMenuButton.click();
});

const startMenuItem = await waitFor(() => getByText('Start'));
const stopMenuItem = getByText('Stop');
const editMenuItem = getByText('Edit');
const deleteMenuItem = getByText('Delete');

expect(startMenuItem).toBeInTheDocument();
expect(stopMenuItem).toBeInTheDocument();
expect(editMenuItem).toBeInTheDocument();
expect(deleteMenuItem).toBeInTheDocument();
});


test('disables stop action if app is not running', async () => {
const { getByTestId, getByText } = render(
<RecoilRoot>
<QueryClientProvider client={queryClient}>
<AppCard
id="1"
title="Test App"
username="Developer"
framework="Some Framework"
url="/some-url"
serverStatus="Pending" // App is not running
isShared={false}
app={{ id: '1', name: 'Test App', framework: 'Some Framework',
description: 'Test App 1',
url: '/user/test/test-app-1/',
thumbnail: '',
username: 'test',
ready: true,
public: false,
shared: false,
last_activity: new Date(),
pending: true,
stopped: false,
status: 'false' }}
/>
</QueryClientProvider>
</RecoilRoot>,
);

// Open context menu first
const contextMenuButton = getByTestId('context-menu-button-card-menu-1');
act(() => {
contextMenuButton.click();
});

const stopMenuItem = await waitFor(() => getByText('Stop'));
expect(stopMenuItem).toBeInTheDocument();
expect(stopMenuItem).toHaveAttribute('aria-disabled', 'true');
});


test('disables edit and delete for shared apps', async () => {
const { getByTestId, getByText } = render(
<RecoilRoot>
<QueryClientProvider client={queryClient}>
<AppCard
id="1"
title="Shared App"
username="Other User"
framework="Some Framework"
url="/some-url"
serverStatus="Ready"
isShared={true} // App is shared
/>
</QueryClientProvider>
</RecoilRoot>,
);

// Open context menu first
const contextMenuButton = getByTestId('context-menu-button-card-menu-1');
act(() => {
contextMenuButton.click();
});

const editMenuItem = await waitFor(() => getByText('Edit'));
const deleteMenuItem = getByText('Delete');

expect(editMenuItem).toHaveAttribute('aria-disabled', 'true');
expect(deleteMenuItem).toHaveAttribute('aria-disabled', 'true');
});


});
39 changes: 33 additions & 6 deletions ui/src/components/app-card/app-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import LockRoundedIcon from '@mui/icons-material/LockRounded';
import PublicRoundedIcon from '@mui/icons-material/PublicRounded';
import PushPinRoundedIcon from '@mui/icons-material/PushPinRounded';
import { Box, Link, Tooltip } from '@mui/material';
import { useQuery } from '@tanstack/react-query';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CardMedia from '@mui/material/CardMedia';
Expand All @@ -26,6 +27,8 @@ import {
} from '../../store';
import ContextMenu, { ContextMenuItem } from '../context-menu/context-menu';
import './app-card.css';
import axios from 'axios';
import { UserState } from '@src/types/user';
interface AppCardProps {
id: string;
title: string;
Expand Down Expand Up @@ -69,12 +72,23 @@ export const AppCard = ({
const [, setIsDeleteOpen] = useRecoilState<boolean>(isDeleteOpen);
const [, setIsStartNotRunningOpen] = useRecoilState(isStartNotRunningOpen);


useEffect(() => {
if (serverStatus) {
setAppStatus(serverStatus);
}
}, [serverStatus, setNotification]);

// Fetch user data and check admin status
const { data: currentUserData } = useQuery<UserState>({
queryKey: ['current-user'],
queryFn: () =>
axios.get('/user').then((response) => {
return response.data;
}),
enabled: true,
});

const getIcon = () => {
if (!isAppCard)
return (
Expand Down Expand Up @@ -110,21 +124,32 @@ export const AppCard = ({
id: 'start',
title: 'Start',
onClick: () => {
// Allow admins to start shared apps
if (isShared && !currentUserData?.admin) {
// Show error if it's a shared app
setNotification('You don\'t have permission to start this app. Please ask the owner to start it.');
return;
}
setIsStartOpen(true);
setCurrentApp(app!); // Add the non-null assertion operator (!) to ensure that app is not undefined
},
visible: true,
disabled: serverStatus !== 'Ready',
disabled: serverStatus !== 'Ready', // Disable start if the app is already running
},
{
id: 'stop',
title: 'Stop',
onClick: () => {
// Allow admins to stop shared apps
if (isShared && !currentUserData?.admin) {
setNotification('You don\'t have permission to stop this app. Please ask the owner to stop it.');
return;
}
setIsStopOpen(true);
setCurrentApp(app!);
setCurrentApp(app!);
},
visible: true,
disabled: serverStatus !== 'Running' || isShared,
disabled: serverStatus !== 'Running', // Disable stop if the app is not running
},
{
id: 'edit',
Expand All @@ -142,11 +167,13 @@ export const AppCard = ({
setCurrentApp(app!);
},
visible: true,
disabled: isShared || id === '' || !isAppCard,
disabled: isShared || id === '' || !isAppCard, // Disable delete for shared apps
danger: true,
},
];




const getHoverClass = (id: string) => {
const element = document.querySelector(
`#card-content-container-${id}`,
Expand Down Expand Up @@ -194,7 +221,7 @@ export const AppCard = ({
url: app?.url || '',
ready: app?.ready || false,
public: app?.public || false,
shared: false,
shared: app?.shared || isShared || false,
last_activity: new Date(app?.last_activity || ''),
status: 'Ready',
});
Expand Down
24 changes: 24 additions & 0 deletions ui/src/data/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,30 @@ export const serverApps = {
username: 'Test User',
},
},
{
name: 'shared-appII',
url: '/shared/test/shared-app/',
started: '2021-02-01T00:00:00.000Z',
pending: null,
ready: true,
last_activity: getMockDate(48),
stopped: true,
user_options: {
name: 'shared-appII',
jhub_app: true,
display_name: 'Shared App Stopped',
description:
'Lorem ipsum dolor sit amet consectetur. Sit vestibulum facilisis auctor pulvinar ac. Cras.',
thumbnail: DEFAULT_APP_LOGO,
framework: 'panel',
conda_env: 'env-1',
profile: 'small0',
env: null,
public: false,
keep_alive: false,
username: 'Test User',
},
},
],
};

Expand Down
6 changes: 3 additions & 3 deletions ui/src/pages/home/apps-section/apps-section.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ describe('AppsSection', () => {
</QueryClientProvider>
</RecoilRoot>,
);
expect(baseElement.querySelectorAll('.card')).toHaveLength(5);
expect(baseElement.querySelectorAll('.card')).toHaveLength(6);
});

test('renders with mocked data and no current user', async () => {
Expand All @@ -63,7 +63,7 @@ describe('AppsSection', () => {
</QueryClientProvider>
</RecoilRoot>,
);
expect(baseElement.querySelectorAll('.card')).toHaveLength(5);
expect(baseElement.querySelectorAll('.card')).toHaveLength(6);
});

test('renders a message when no apps', () => {
Expand Down Expand Up @@ -130,7 +130,7 @@ describe('AppsSection', () => {
fireEvent.change(input, { target: { value: 'panel' } });
});
const cards = baseElement.querySelectorAll('.card');
expect(cards).toHaveLength(2);
expect(cards).toHaveLength(3);
});

test('should toggle app table view', async () => {
Expand Down
Loading

0 comments on commit 4d9f3b9

Please sign in to comment.