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

Jh/disable module #68

Open
wants to merge 22 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
2fde840
Added disable module checkbox to UI
jhurst502 Sep 15, 2021
ff55728
Disabled modules will not show on explore page. Title of diabled modu…
jhurst502 Sep 16, 2021
6004fb6
Start button grayed out with tooltip on disable. Added react-tooltip …
jhurst502 Sep 17, 2021
17b4892
Creators should be able to disable only their own modules.
jhurst502 Sep 21, 2021
783afef
Resolved merge conflicts
jhurst502 Oct 30, 2021
2eb6697
Remove code smells
jhurst502 Oct 31, 2021
f42022d
Update src/components/ModuleCard/ModuleCard.tsx
jhurst502 Nov 7, 2021
fbcc4a7
Update src/pages/Explore/Explore.tsx
jhurst502 Nov 7, 2021
75a2804
Update src/pages/ModuleEditor/ModuleEditor.tsx
jhurst502 Nov 7, 2021
e101e08
Update src/util/LoadingButton.tsx
jhurst502 Nov 7, 2021
05cb83a
Added hooks.ts selector and refactored existing useSelector
jhurst502 Nov 7, 2021
c89e7ce
Fixed start button not graying out for disabled modules
jhurst502 Nov 7, 2021
0140951
Resolved merge conflicts
jhurst502 Nov 17, 2021
602ed69
Fixed TSLint issue in module card
jhurst502 Nov 17, 2021
6676b26
Loading button tool tip text is passed as prop
jhurst502 Nov 17, 2021
21a92a7
Merged dev
jhurst502 Dec 28, 2021
1ff45e2
Moved React tooltip into the IconButton
jhurst502 Jan 3, 2022
d55ca99
Renamed toolTipText to disabledToolTipText
jhurst502 Jan 3, 2022
c4204cd
Merged dev
jhurst502 Jan 3, 2022
4a306bc
Merge branch 'dev' into jh/disable-module
jasekiw Apr 10, 2022
a29c6ad
run yarn install after merge
jasekiw Apr 10, 2022
4f4775c
Add default while loading the module
jasekiw Apr 10, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
"react-router-bootstrap": "^0.25.0",
"react-router-dom": "^5.1.0",
"react-select": "^3.1.0",
"react-tooltip": "^4.2.21",
"react-tag-autocomplete": "^6.3.0",
"redux": "^4.0.4",
"redux-persist": "^6.0.0",
Expand Down
4 changes: 2 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ class App extends React.Component {

public render() {
return (
<CapsLockContextProvider>
<CapsLockContextProvider>
<Routes/>
</CapsLockContextProvider>
</CapsLockContextProvider>
);
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export async function resetVm(id: number): Promise<string> {
}

export async function getModule(id: number) {
return ( await api.get<Module>(`/modules/${id}`)).data;
return ( await api.get<Module>(`/module/${id}`)).data;
jhurst502 marked this conversation as resolved.
Show resolved Hide resolved
}

export async function getUserList() {
Expand Down
23 changes: 14 additions & 9 deletions src/components/LabEnvironment/LabEnvironment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {Button, Col, Container, Dropdown, ButtonGroup, ListGroup, Row, Tab} from
import {Status} from '../../pages/Status/Status';
import {Document, Page, pdfjs} from 'react-pdf';
import {PDFDocumentProxy} from 'pdfjs-dist';
import {getUserLabReadmeUrl, getUserLabTopologyUrl, updateEndDateTime} from '../../api';
import {getUserLabReadmeUrl, getUserLabTopologyUrl, updateEndDateTime,getPublicModule} from '../../api';
import {UserLab} from '../../types/UserLab';
import {LoadingButton} from '../../util/LoadingButton';
import {combineClasses, getRemainingLabTime, getLuxonObjectFromString} from '../../util';
Expand All @@ -14,6 +14,7 @@ import {VmStatusIndicator} from '../util/VmStatusIndicator/VmStatusIndicator';
import styles from './LabEnvironment.module.scss';
import {RoutePaths} from 'router/RoutePaths';
import Popup from '../util/Popup';
import {Module} from '../../types/Module';

pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js`;

Expand All @@ -30,6 +31,7 @@ interface LabEnvironmentState {
readmeLoaded: boolean;
show_vm: boolean;
eventKey: string;
module?: Module;
labEndDateTime?: string;
interval: number;
}
Expand All @@ -46,14 +48,15 @@ export class LabEnvironment extends Component<LabEnvironmentProps, LabEnvironmen
interval: 0
};

componentDidMount() {
this.setLabEndDateTimeInterval(this.props.userLab);
}

componentWillUnmount() {
window.clearInterval(this.state.interval);
}

async componentDidMount() {
this.setLabEndDateTimeInterval(this.props.userLab);
this.setState({module: await getPublicModule(this.props.userLab.userModuleId)});
}

canGoToPrevPage = () => this.state.pageNumber > 1;
canGoToNextPage = () => this.state.pageNumber < this.state.numPages;

Expand Down Expand Up @@ -101,7 +104,7 @@ export class LabEnvironment extends Component<LabEnvironmentProps, LabEnvironmen
};
return (
<Popup
id='times-up'
id='times-up'
title='Times up!'
description='Congratulations! You have finished working on this lab.
You will be automatically redirected to your modules page after 15 seconds.'
Expand All @@ -110,7 +113,7 @@ export class LabEnvironment extends Component<LabEnvironmentProps, LabEnvironmen
display={true}
/>
);
} else if (remainingLabTime > 0 && remainingLabTime <= 900) {
} else if (remainingLabTime > 0 && remainingLabTime <= 900) {
return <Button className='ml-2' onClick={this.onExtendEndDateTime}>Extend</Button>;
} else {
return;
Expand All @@ -126,9 +129,11 @@ export class LabEnvironment extends Component<LabEnvironmentProps, LabEnvironmen
<h2>Lab : {this.props.userLab.lab.name}</h2>
{this.isLabAbleToStart() ?
<LoadingButton
loading={this.props.starting}
loading={this.props.starting || !this.state.module}
disabled={this.state.module?.disabled ?? true}
label='Start Lab'
onClick={this.props.onStartLab}
onClick={this.props.onStartLab}
disabledToolTipText={'This lab is currently disabled'}
/> : this.props.userLab.lab.type === 'Temporary' &&
<h6 style={{textAlign: 'right'}}>Lab's time remaining: {getRemainingLabTime(this.state.labEndDateTime)}{this.showExtendButton()}</h6>
}
Expand Down
10 changes: 7 additions & 3 deletions src/components/LabListEditor/LabListEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@ interface Props {
labs: LabItem[];
prefix: string;
moduleId: number;
disabledModule: boolean;
}
function getNewLabEditorLink(moduleId: number) {
return RoutePaths.EditLab.replace(':moduleId', String(moduleId)).replace(':labId', '');
}

export function LabListEditor({labs, prefix, moduleId}: Props) {


export function LabListEditor({labs, prefix, moduleId, disabledModule}: Props) {
return (
<FieldArray
name={prefix}
Expand All @@ -30,8 +33,9 @@ export function LabListEditor({labs, prefix, moduleId}: Props) {
<IconButton
icon={faPlusCircle}
size={'2x'}
link={getNewLabEditorLink(moduleId)}
color={'black'}
tooltip={disabledModule ? '"Disable Module" must be unchecked to add a Lab' : undefined}
color={disabledModule ? 'grey' : 'black'}
link={!disabledModule ? getNewLabEditorLink(moduleId) : undefined}
/>
</Col>
</Row>
Expand Down
5 changes: 2 additions & 3 deletions src/components/ModuleCard/ModuleCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ class ModuleCardComponent extends Component<ModuleCardProps, ModuleCardState> {
return (
<Card className={Styles.card}>
<Card.Body style={{height: 270}}>
<Card.Title>{module.name}</Card.Title>
<Card.Title>{module.name} {(module.disabled ? '(disabled)' : '')}</Card.Title>
{!this.state.tagsExpanded ?
<Card.Text style={{textAlign: 'left', fontSize: '1rem', fontWeight: 400, color: '#868e96'}}>
<Card.Text style={{height: 105, textOverflow: 'ellipsis', overflow: 'hidden', textAlign: 'left', fontSize: '1rem', fontWeight: 400, color: '#868e96'}}>
{module.description.substring(0, 150)}
</Card.Text> : null}
{module.moduleTags.length !== 0 &&
Expand All @@ -60,7 +60,6 @@ class ModuleCardComponent extends Component<ModuleCardProps, ModuleCardState> {
);
}


getStartButton() {
if (this.props.buttonLink) {
return (
Expand Down
24 changes: 14 additions & 10 deletions src/components/util/IconButton/IconButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {IconProp, SizeProp} from '@fortawesome/fontawesome-svg-core';
import styles from './IconButton.module.scss';
import {Link} from 'react-router-dom';
import ReactTooltip from 'react-tooltip';

interface IconButtonProps {
tooltip?: string;
icon: IconProp;
hideIcon?: boolean;
size?: SizeProp;
Expand Down Expand Up @@ -49,16 +51,18 @@ export function IconButton(props: IconButtonProps) {
function IconButtonIcon(props: IconButtonProps) {
return (
<React.Fragment>
<FontAwesomeIcon
icon={props.icon}
size={props.size}
className={styles['icon']}
style={{
cursor: 'pointer',
...(props.customSize ? {fontSize: props.customSize} : {}),
...(props.color ? {color: props.color} : {})
}}
/>
<ReactTooltip place='left' type='dark' effect='solid'/>
<FontAwesomeIcon
data-tip={props.tooltip}
icon={props.icon}
size={props.size}
className={styles['icon']}
style={{
cursor: 'pointer',
...(props.customSize ? {fontSize: props.customSize} : {}),
...(props.color ? {color: props.color} : {})
}}
/>
{props.children ? <span style={{marginLeft: '1rem'}}>{props.children}</span> : null}
</React.Fragment>
);
Expand Down
3 changes: 2 additions & 1 deletion src/factories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ export function makeModuleForm(): ModuleForm {
description: '',
name: '',
published: false,
disabled: false,
specialCode: uuid(),
type: 'SingleUser',
userId: 0
ownerId: 0
};
}

Expand Down
2 changes: 1 addition & 1 deletion src/pages/Explore/Explore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class Explore extends React.Component<{}, ExploreState> {
async loadModules() {
try {
const modules = await getPublicModules();
this.setState({modules: modules, state: 'success'});
this.setState({modules: modules.filter(m => !m.disabled), state: 'success'});
} catch (_) {
this.setState({state: 'error'});
}
Expand Down
14 changes: 12 additions & 2 deletions src/pages/ModuleEditor/ModuleEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {PageTitle} from '../../components/util/PageTitle';
import {TagEditor} from '../../components/TagEditor/TagEditor';
import {ModuleTag} from '../../types/ModuleTag';
import {useEffect} from 'react';
import {useUser} from '../../redux/selectors/hooks';

const moduleTypeOptions: DropdownOption<ModuleType>[] = [
{value: 'SingleUser', label: 'Single User'},
Expand All @@ -47,6 +48,8 @@ export default function ModuleEditor({match: {params: {moduleId}}}: Props) {
setMessage(undefined);
}

const user = useUser();

const onSubmit = async (form: ModuleForm, {setErrors}: FormikHelpers<ModuleForm>) => {
setMessage(undefined);
try {
Expand Down Expand Up @@ -93,6 +96,8 @@ export default function ModuleEditor({match: {params: {moduleId}}}: Props) {
LoadModule();
}, [moduleId]);

const canDisable = user?.role === 'Admin' || (user?.role === 'Creator' && user?.id === initialValues.ownerId);

const renderForm = () => (
<Formik
initialValues={initialValues}
Expand All @@ -113,7 +118,7 @@ export default function ModuleEditor({match: {params: {moduleId}}}: Props) {
<Col className='d-flex justify-content-end align-items-center'>
{editing && Boolean(values.id) && <Button style={{marginRight: '1rem'}} type='button' variant='danger' onClick={onCancel}>Cancel</Button>}
{!editing && <Button type='button' onClick={() => setEditing(true)}>Edit</Button>}
{editing && <LoadingButton loading={isSubmitting} type='submit' label={values.id ? 'Save' : 'Create'}/>}
{editing && <LoadingButton loading={isSubmitting} type='submit' label={values.id ? 'Save' : 'Create'} disabledToolTipText={'This lab is currently disabled'}/>}
</Col>
</Row>
<Col sm='12' className='m-auto'>
Expand All @@ -131,6 +136,11 @@ export default function ModuleEditor({match: {params: {moduleId}}}: Props) {
<Form.Group>
<CheckBoxInput name={propertyOf<ModuleForm>('published')} label='Publish (Display on the explore page)' disabled={!editing}/>
</Form.Group>
{canDisable &&
<Form.Group>
<CheckBoxInput name={propertyOf<ModuleForm>('disabled')} label='Disable Module' disabled={!editing}/>
</Form.Group>
}
<Form.Group>
<Form.Label column={true}>Type</Form.Label>
<DropdownInput name={propertyOf<ModuleForm>('type')} dropdownData={moduleTypeOptions} disabled={!editing}/>
Expand All @@ -155,7 +165,7 @@ export default function ModuleEditor({match: {params: {moduleId}}}: Props) {
Once you save your changes you can add and remove labs from this module. Note: Adding or editing a lab will cancel changes on this page
</p>
{values.id !== 0 &&
<LabListEditor labs={values.labs} prefix={propertyOf<ModuleForm>('labs')} moduleId={values.id}/>
<LabListEditor labs={values.labs} prefix={propertyOf<ModuleForm>('labs')} moduleId={values.id} disabledModule={values.disabled}/>
}
</Col>
</Form>
Expand Down
3 changes: 2 additions & 1 deletion src/pages/ModuleEditor/ModuleEditorSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ export const ModuleEditorSchema = object<ModuleForm>({
name: string().min(3, 'Must at least be 3 characters').required('Required'),
specialCode : string().required('Required'),
description: string().required('Required').min(4, 'Required'),
disabled: boolean(),
updatedAt: string().notRequired(),
createdAt: string().notRequired(),
id: number(),
userId: number(),
type: string() as StringSchema<ModuleType>,
ownerId: number(),
jhurst502 marked this conversation as resolved.
Show resolved Hide resolved
published: boolean(),
labs: array(),
moduleTags: array()
Expand Down
4 changes: 3 additions & 1 deletion src/pages/PublicModule/PublicModule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@ class PublicModule extends Component<PublicModuleProps, MyModuleState> {
id: 0,
createdAt: '',
description: '',
disabled: false,
name: 'Loading',
published: false,
userId: 0,
ownerId: 0,
specialCode: '',
type: 'SingleUser',
updatedAt: '',
Expand Down Expand Up @@ -108,6 +109,7 @@ class PublicModule extends Component<PublicModuleProps, MyModuleState> {
loading={this.state.startingModule}
className='btn btn-primary'
onClick={this.hasUserModule() ? undefined : this.startModule}
disabledToolTipText={'This lab is currently disabled'}
/>
);
}
Expand Down
3 changes: 2 additions & 1 deletion src/pages/UserModulePage/UserModulePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ class UserModulePage extends Component <UserModuleLabsProps, UserModuleLabsState
id: 0,
name: '',
description: '',
userId: 0,
disabled: false,
ownerId: 0,
published: false,
updatedAt: '',
createdAt: '',
Expand Down
7 changes: 7 additions & 0 deletions src/redux/selectors/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {getCurrentUser} from './entities';
import {useSelector} from 'react-redux';
import {User} from '../../types/User';

export function useUser(): User | null {
return useSelector(getCurrentUser);
}
3 changes: 2 additions & 1 deletion src/types/Module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import {ModuleTag} from './ModuleTag';
export interface BaseModule extends TrackableEntity {
name: string;
description: string;
disabled: boolean;
published: boolean;
specialCode: string;
type: ModuleType;
userId: number;
ownerId: number;
moduleTags: ModuleTag[];
}

Expand Down
1 change: 1 addition & 0 deletions src/types/UserLab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface UserLab extends Entity {
hasReadme: boolean;
endDateTime?: string;
status: UserLabStatus;
userModuleId: number;
}

export type UserLabStatus = 'Started' | 'NotStarted' | 'Completed';
Expand Down
40 changes: 30 additions & 10 deletions src/util/LoadingButton.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as React from 'react';
import {Button, ButtonProps, Spinner} from 'react-bootstrap';
import ReactTooltip from 'react-tooltip';

type onClick = ((e: React.SyntheticEvent<HTMLButtonElement>) => void) | (() => void) | (() => any);

Expand All @@ -10,17 +11,36 @@ interface Props {
type?: ButtonProps['type'];
disabled?: boolean;
onClick?: onClick;
disabledToolTipText?: string;
}

function renderButton(props: Props) {
const innerButton = (
<Button disabled={props.loading || props.disabled} variant='primary' type={props.type || 'submit'} className={props.className} onClick={props.onClick}>
{props.loading ?
<Spinner
as='span'
animation='border'
size='sm'
role='status'
aria-hidden='true'
/> : props.label}
</Button>
);

if (props.disabled) {
return (<>
<ReactTooltip place='left' type='dark' effect='solid'/>
<span data-tip={props.disabledToolTipText}>
{innerButton}
</span>
</>);
}
return innerButton;
}

export const LoadingButton = (props: Props) => (
<Button disabled={props.loading || props.disabled} variant='primary' type={props.type || 'submit'} className={props.className} onClick={props.onClick}>
{ props.loading ?
<Spinner
as='span'
animation='border'
size='sm'
role='status'
aria-hidden='true'
/> : props.label}
</Button>
<>
{renderButton(props)}
</>
);
Loading