Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

task/WP-273: Category icon #874

Merged
merged 13 commits into from
Oct 25, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ const AppBrowser = () => {
}
>
<span className="nav-content">
<AppIcon appId={app.appId} />
<AppIcon appId={app.appId} category={category} />
<span className="nav-text">{app.label || app.appId}</span>
</span>
</NavLink>
Expand Down
14 changes: 13 additions & 1 deletion client/src/components/Applications/AppForm/AppForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,18 @@ const AdjustValuesWhenQueueChanges = ({ app }) => {
};

const AppInfo = ({ app }) => {
const categoryDict = useSelector((state) => state.apps.categoryDict);
const getAppCategory = (appId) => {
for (const [cat, apps] of Object.entries(categoryDict)) {
if (apps.some((app) => app.appId === appId)) {
return cat;
}
}
return null;
};

const appCategory = getAppCategory(app.definition.id);

return (
<div className="appInfo-wrapper">
<h5 className="appInfo-title">{app.definition.label}</h5>
Expand All @@ -163,7 +175,7 @@ const AppInfo = ({ app }) => {
target="_blank"
rel="noreferrer noopener"
>
<AppIcon appId={app.definition.id} />{' '}
<AppIcon appId={app.definition.id} category={appCategory} />{' '}
<span>{app.definition.notes.label} Documentation</span>
</a>
) : null}
Expand Down
18 changes: 16 additions & 2 deletions client/src/components/_common/AppIcon/AppIcon.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@ import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import Icon from '_common/Icon';
import './AppIcon.scss';
import iconStyles from '../../../styles/trumps/icon.css';
import iconFontsStyles from '../../../styles/trumps/icon.fonts.css';
import doesClassExist from 'utils/doesClassExist';

const AppIcon = ({ appId }) => {
const AppIcon = ({ appId, category }) => {
const appIcons = useSelector((state) => state.apps.appIcons);
const findAppIcon = (id) => {
let appIcon = 'applications';
let appIcon = category
? category.replace(' ', '-').toLowerCase()
: 'applications';
Object.keys(appIcons).forEach((appName) => {
if (id.includes(appName)) {
appIcon = appIcons[appName].toLowerCase();
Expand All @@ -19,6 +24,10 @@ const AppIcon = ({ appId }) => {
} else if (id.includes('extract')) {
appIcon = 'extract';
}
// Check if the CSS class exists, if not default to 'icon-applications'
if (!doesClassExist(`icon-${appIcon}`, [iconFontsStyles, iconStyles])) {
appIcon = 'applications';
}
return appIcon;
};
const iconName = findAppIcon(appId);
Expand All @@ -27,6 +36,11 @@ const AppIcon = ({ appId }) => {
};
AppIcon.propTypes = {
appId: PropTypes.string.isRequired,
category: PropTypes.string,
};

AppIcon.defaultProps = {
category: 'applications',
};

export default AppIcon;
58 changes: 40 additions & 18 deletions client/src/components/_common/AppIcon/AppIcon.test.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import React from 'react';
import { render } from '@testing-library/react';
import {
toHaveAttribute,
toHaveTextContent,
} from '@testing-library/jest-dom/dist/matchers';
import '@testing-library/jest-dom/extend-expect';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import AppIcon from './AppIcon';
Expand All @@ -15,43 +12,68 @@ const store = mockStore({
jupyter: 'jupyter',
},
},
categories: {
visualization: ['vasp'],
'data-processing': ['jupyter'],
},
});

expect.extend({ toHaveAttribute });
// Mock document.styleSheets to simulate the existence of the CSS classes we're testing for
Object.defineProperty(document, 'styleSheets', {
value: [
{
cssRules: [
{ selectorText: '.icon-jupyter::before' },
{ selectorText: '.icon-visualization::before' },
{ selectorText: '.icon-compress::before' },
{ selectorText: '.icon-extract::before' },
],
},
],
writable: true,
});

function renderAppIcon(appId) {
function renderAppIcon(appId, category = 'default') {
return render(
<Provider store={store}>
<AppIcon appId={appId} />
<AppIcon
appId={appId}
category={category}
appIcons={store.getState().apps.appIcons}
/>
</Provider>
);
}

describe('AppIcon', () => {
it('should render icons for known app IDs', () => {
const { getByRole } = renderAppIcon('jupyter');
expect(getByRole('img')).toHaveAttribute('class', 'icon icon-jupyter');
const { container } = renderAppIcon('jupyter', 'data-processing');
expect(container.firstChild).toHaveClass('icon-jupyter');
});
it('should show generic icons for apps with no appIcon', () => {
const { getByRole } = renderAppIcon('vasp');
expect(getByRole('img')).toHaveAttribute('class', 'icon icon-applications');

it('should show category icons for apps with no appIcon', () => {
const { container } = renderAppIcon('vasp', 'visualization');
expect(container.firstChild).toHaveClass('icon-visualization');
});

it('should render icons for prtl.clone apps', () => {
const { getByRole } = renderAppIcon(
const { container } = renderAppIcon(
'prtl.clone.username.allocation.jupyter'
);
expect(getByRole('img')).toHaveAttribute('class', 'icon icon-jupyter');
expect(container.firstChild).toHaveClass('icon-jupyter');
});

it('should render icon for zippy toolbar app', () => {
const { getByRole } = renderAppIcon(
const { container } = renderAppIcon(
'prtl.clone.username.FORK.zippy-0.2u2-2.0'
);
expect(getByRole('img')).toHaveAttribute('class', 'icon icon-compress');
expect(container.firstChild).toHaveClass('icon-compress');
});

it('should render icon for extract toolbar app', () => {
const { getByRole } = renderAppIcon(
const { container } = renderAppIcon(
'prtl.clone.username.FORK.extract-0.1u7-7.0'
);
expect(getByRole('img')).toHaveAttribute('class', 'icon icon-extract');
expect(container.firstChild).toHaveClass('icon-extract');
});
});
16 changes: 16 additions & 0 deletions client/src/utils/doesClassExist.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
function doesClassExist(className, stylesheets) {
for (let stylesheet of stylesheets) {
//Required to make this work with Jest/identity-obj-proxy
if (typeof stylesheet === 'object') {
if (stylesheet[className]) {
return true;
}
} else if (typeof stylesheet === 'string') {
if (stylesheet.includes(`.${className}::before`)) {
return true;
}
}
}
return false;
}
export default doesClassExist;