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

Convert remaining components to ts #79

Merged
merged 13 commits into from
May 24, 2024
Prev Previous commit
Next Next commit
Convert Threatbar to TypeScript and fix issues
Signed-off-by: Johannes Loher <johannes.loher@tngtech.com>
ghost91- committed May 13, 2024
commit d8e92d2b82eec445dce4a28cef749e2486632990
38 changes: 24 additions & 14 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 15 additions & 11 deletions src/client/components/board/board.tsx
Original file line number Diff line number Diff line change
@@ -46,12 +46,13 @@ const Board: FC<BoardProps> = ({
? '/api'
: `${window.location.protocol}//${window.location.hostname}:${API_PORT}`;

const updateName = useCallback(
(index: number, name: string) => {
setNames([...names].splice(index, 1, name));
},
[setNames, names],
);
const updateName = useCallback((index: number, name: string) => {
setNames((names) => {
const newNames = [...names];
newNames[index] = name;
return newNames;
});
}, []);

const apiGetRequest = useCallback(
async (endpoint: string) => {
@@ -88,15 +89,18 @@ const Board: FC<BoardProps> = ({
const model = modelResponse?.body as Record<string, unknown> | undefined;

setModel(model);
}, [apiGetRequest, setModel]);
}, [apiGetRequest]);

// consider using react-query instead
useEffect(() => {
updateNames();
if (G.modelType !== ModelType.IMAGE) {
updateModel();
}
}, [updateNames, G.modelType, updateModel]);
}, [G.modelType, updateModel]);

useEffect(() => {
updateNames();
}, [updateNames]);

const current = playerID === ctx.currentPlayer;

@@ -134,7 +138,7 @@ const Board: FC<BoardProps> = ({
matchID={matchID}
/>
)}
{G.modelType === ModelType.THREAT_DRAGON && (
{G.modelType === ModelType.THREAT_DRAGON && model !== undefined && (
<Model
model={model}
selectedDiagram={G.selectedDiagram}
@@ -158,7 +162,7 @@ const Board: FC<BoardProps> = ({
isInThreatStage={isInThreatStage}
/>
</div>
{playerID && G.suit && (
{playerID && (
<Deck
cards={G.players[Number.parseInt(playerID)]}
suit={G.suit}
2 changes: 1 addition & 1 deletion src/client/components/deck/deck.tsx
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ import { GameMode, getCardCssClass } from '../../../utils/GameMode';
import type { Card, Suit } from '../../../utils/cardDefinitions';

interface DeckProps {
suit: Suit;
suit?: Suit;
cards: Card[];
isInThreatStage: boolean;
round: number;
299 changes: 0 additions & 299 deletions src/client/components/threatbar/threatbar.jsx

This file was deleted.

Original file line number Diff line number Diff line change
@@ -2,27 +2,49 @@ import { render, screen } from '@testing-library/react';
import { GameMode } from '../../../utils/GameMode';
import React from 'react';
import Threatbar from './threatbar';
import type { GameState } from '../../../game/gameState';
import type { Ctx } from 'boardgame.io';
import { ModelType } from '../../../utils/constants';

describe('<Threatbar>', () => {
const G = {
const G: GameState = {
gameMode: GameMode.EOP,
threat: {
modal: false,
new: false,
},
selectedDiagram: 'diagram1',
selectedDiagram: 0,
selectedComponent: 'component1',
identifiedThreats: {
diagram1: {
component1: {
threat1: {
title: 'Identified Threat 1',
modal: false,
new: false,
},
threat2: {
title: 'Identified Threat 2',
modal: false,
new: false,
},
},
},
},
dealt: [],
passed: [],
suit: undefined,
dealtBy: '',
players: [],
round: 0,
numCardsPlayed: 0,
scores: [],
lastWinner: 0,
maxRounds: 0,
selectedThreat: '',
startingCard: '',
turnDuration: 0,
modelType: ModelType.IMAGE,
};

it('shows identified threats in reverse order', () => {
@@ -57,7 +79,16 @@ describe('<Threatbar>', () => {
};

render(
<Threatbar G={G} ctx={{}} moves={{}} active names={[]} model={model} />,
<Threatbar
G={G}
ctx={{} as Ctx}
moves={{}}
active
names={[]}
model={model}
isInThreatStage={false}
playerID={null}
/>,
);

const threats = screen.getAllByText(/^Identified Threat \d+$/);
@@ -98,7 +129,16 @@ describe('<Threatbar>', () => {
};

render(
<Threatbar G={G} ctx={{}} moves={{}} active names={[]} model={model} />,
<Threatbar
G={G}
ctx={{} as Ctx}
moves={{}}
active
names={[]}
model={model}
isInThreatStage={false}
playerID={null}
/>,
);

const threats = screen.getAllByText(/^Existing Threat \d+$/);
294 changes: 294 additions & 0 deletions src/client/components/threatbar/threatbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
import {
faBolt,
faEdit,
faPlus,
faTrash,
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import React from 'react';
import type { FC } from 'react';
import nl2br from 'react-nl2br';
import {
Button,
Card,
CardBody,
CardFooter,
CardHeader,
CardText,
Col,
Collapse,
ListGroup,
ListGroupItem,
Row,
} from 'reactstrap';
import confirm from 'reactstrap-confirm';
import { getSuitDisplayName } from '../../../utils/cardDefinitions';
import { getComponentName } from '../../../utils/utils';
import ThreatModal from '../threatmodal/threatmodal';
import './threatbar.css';
import type { GameState } from '../../../game/gameState';
import type { BoardProps } from 'boardgame.io/react';
import type { Threat } from '../../../game/threat';

type ThreatbarProps = {
model?: Record<string, any>; // TODO: improve
active: boolean;
names: string[];
isInThreatStage: boolean;
} & Pick<BoardProps<GameState>, 'G' | 'ctx' | 'moves' | 'playerID'>;

const Threatbar: FC<ThreatbarProps> = ({
playerID,
model,
G,
ctx,
moves,
active,
names,
isInThreatStage,
}) => {
const getSelectedComponent = () => {
if (G.selectedComponent === '' || model === undefined) {
return null;
}

const diagram = model.detail.diagrams[G.selectedDiagram].diagramJson;
for (let i = 0; i < diagram.cells.length; i++) {
const cell = diagram.cells[i];
if (cell.id === G.selectedComponent) {
console.log("Cell:", cell);
return cell;
}
}

return null;
};

const getThreatsForSelectedComponent = (): Threat[] => {
const threats: Threat[] = [];
if (G.selectedComponent === '' || model === undefined) {
return threats;
}

const diagram = model.detail.diagrams[G.selectedDiagram].diagramJson;
for (let i = 0; i < diagram.cells.length; i++) {
const cell = diagram.cells[i];
if (G.selectedComponent !== '') {
if (cell.id === G.selectedComponent) {
if (Array.isArray(cell.threats)) {
// fix threat ids
for (let j = 0; j < cell.threats.length; j++) {
if (!('id' in cell.threats[j])) {
cell.threats[j].id = j + '';
}
}
return cell.threats;
}
}
} else {
/*
if (Array.isArray(cell.threats)) {
threats = threats.concat(cell.threats);
}
*/
}
}
return threats;
};

const getIdentifiedThreatsForSelectedComponent = () => {
const threats = [];
if (G.selectedDiagram in G.identifiedThreats) {
if (G.selectedComponent in G.identifiedThreats[G.selectedDiagram]) {
for (const k in G.identifiedThreats[G.selectedDiagram][
G.selectedComponent
]) {
const t =
G.identifiedThreats[G.selectedDiagram][G.selectedComponent][k];
threats.push(t);
}
}
}

return threats;
};

const threats = getThreatsForSelectedComponent().reverse();
const identifiedThreats =
getIdentifiedThreatsForSelectedComponent().reverse();
const component = getSelectedComponent();
const componentName = getComponentName(component);

return (
<div className="threat-bar" hidden={G.selectedComponent === ''}>
<Card>
<CardHeader>
Threats for {componentName}{' '}
{/* @ts-expect-error @fortawesome/react-fontawesome uses an older version of @fortawesome/fontawesome-svg-core (1.3.0), which makes the types incompatible. It still works correctly at runtime. */}
<FontAwesomeIcon style={{ float: 'right' }} icon={faBolt} />
</CardHeader>
<CardBody className="threat-container">
{playerID && (
<Button
color="primary"
size="lg"
block
disabled={
G.selectedComponent === '' ||
!isInThreatStage ||
G.passed.includes(playerID) ||
!active
}
onClick={() => moves.toggleModal()}
>
{/* @ts-expect-error @fortawesome/react-fontawesome uses an older version of @fortawesome/fontawesome-svg-core (1.3.0), which makes the types incompatible. It still works correctly at runtime. */}
<FontAwesomeIcon icon={faPlus} /> Add Threat
</Button>
)}
<div hidden={component !== null && component.type !== 'tm.Flow'}>
<hr />
<Card>
<CardHeader>Flow Data Elements</CardHeader>
<ListGroup flush>
{component !== null &&
Array.isArray(component.dataElements) &&
component.dataElements.map((val: any, idx: number) => {
console.log("data elements: ", val, idx);
return (
<ListGroupItem className="thin-list-group-item" key={idx}>
{val}
</ListGroupItem>
);
})}
{component !== null && !Array.isArray(component.dataElements) && (
<ListGroupItem>
<em>No data elements defined</em>
</ListGroupItem>
)}
</ListGroup>
</Card>
</div>
<hr />
{identifiedThreats.map((val, idx) => (
<Card key={idx}>
<CardHeader
className="hoverable"
onClick={() => moves.selectThreat(val.id)}
>
<strong>{val.title}</strong>
<Row>
<Col xs="6">
<small>
{val.type !== undefined
? getSuitDisplayName(G.gameMode, val.type)
: ''}
</small>
</Col>
<Col xs="3">
<small>{val.severity}</small>
</Col>
<Col xs="3">
<small className="float-right">
&mdash;{' '}
{val.owner !== undefined
? names[Number.parseInt(val.owner)]
: ''}
</small>
</Col>
</Row>
</CardHeader>
<Collapse isOpen={G.selectedThreat === val.id}>
<CardBody>
<CardText>{nl2br(val.description)}</CardText>
<hr />
<CardText>{nl2br(val.mitigation)}</CardText>
</CardBody>
<CardFooter hidden={val.owner !== playerID}>
<Row>
<Col xs="6">
<Button
block
onClick={() => moves.toggleModalUpdate(val)}
>
{/* @ts-expect-error @fortawesome/react-fontawesome uses an older version of @fortawesome/fontawesome-svg-core (1.3.0), which makes the types incompatible. It still works correctly at runtime. */}
<FontAwesomeIcon icon={faEdit} /> Update
</Button>
</Col>
<Col xs="6">
<Button
block
color="danger"
onClick={() =>
confirm().then((result) => {
if (result) {
moves.deleteThreat(val);
}
})
}
>
{/* @ts-expect-error @fortawesome/react-fontawesome uses an older version of @fortawesome/fontawesome-svg-core (1.3.0), which makes the types incompatible. It still works correctly at runtime. */}
<FontAwesomeIcon icon={faTrash} /> Remove
</Button>
</Col>
</Row>
</CardFooter>
</Collapse>
</Card>
))}
{identifiedThreats.length <= 0 && (
<em className="text-muted">
No threats identified for this component yet.
</em>
)}
<hr />
{threats.map((val: Threat, idx: number) => (
<Card key={idx}>
<CardHeader
className="hoverable"
onClick={() => moves.selectThreat(val.id)}
>
<strong>{val.title}</strong>
<Row>
<Col xs="6">
<small>{val.type}</small>
</Col>
<Col xs="3">
<small>{val.severity}</small>
</Col>
<Col xs="3">
<small className="float-right">
&mdash;{' '}
{typeof val.owner !== 'undefined' ? val.owner : 'NA'}
</small>
</Col>
</Row>
</CardHeader>
<Collapse isOpen={G.selectedThreat === val.id}>
<CardBody>
<CardText>{nl2br(val.description)}</CardText>
<hr />
<CardText>{nl2br(val.mitigation)}</CardText>
</CardBody>
</Collapse>
</Card>
))}
{threats.length <= 0 && (
<em className="text-muted">
No existing threats for this component.
</em>
)}
</CardBody>
</Card>
<ThreatModal
isOpen={G.threat.modal}
G={G}
ctx={ctx}
playerID={playerID}
moves={moves}
names={names}
/>
</div>
);
};

export default Threatbar;
19 changes: 19 additions & 0 deletions src/types/reactstrap-confirm.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
declare module 'reactstrap-confirm' {
type Props = {
message?: React.ReactNode;
title?: React.ReactNode;
confirmTex?: React.ReactNode;
cancelText?: React.ReactNode;
confirmColor?: 'string';
cancelColor?: string;
className?: string;
size?: string | null;
buttonsComponent?: React.ComponentType | null;
bodyComponent?: React.ComponentType | null;
modalProps?: React.ComponentProps<import('reactstrap').Modal>;
};

const confirm: (props?: Props) => Promise<boolean>;

export default confirm;
}