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

✨ Dynamic assess button and view assessments page #1325

Merged
merged 1 commit into from
Sep 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion client/src/app/Paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export enum Paths {
applicationsImportsDetails = "/applications/application-imports/:importId",
applicationsAssessment = "/applications/assessment/:assessmentId",
assessmentActions = "/applications/assessment-actions/:applicationId",
assessmentSummary = "/applications/assessment-summary/:assessmentId",
applicationsReview = "/applications/application/:applicationId/review",
applicationsAnalysis = "/applications/analysis",
archetypes = "/archetypes",
Expand Down Expand Up @@ -40,7 +41,7 @@ export enum Paths {
proxies = "/proxies",
migrationTargets = "/migration-targets",
assessment = "/assessment",
questionnaire = "/questionnaire",
questionnaire = "/questionnaire/:questionnaireId",
jira = "/jira",
}

Expand Down
14 changes: 13 additions & 1 deletion client/src/app/Routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,26 @@
"./pages/assessment-management/assessment-settings/assessment-settings-page"
)
);

const Questionnaire = lazy(
() => import("./pages/assessment-management/questionnaire/questionnaire-page")
);

const AssessmentActions = lazy(
() =>
import("./pages/applications/assessment-actions/assessment-actions-page")
);
const Archetypes = lazy(() => import("./pages/archetypes/archetypes-page"));

const AssessmentSummary = lazy(
() =>
import(
"./pages/applications/application-assessment/components/assessment-summary/assessment-summary-page"
)
);
export interface IRoute {
path: string;
comp: React.ComponentType<any>;

Check warning on line 62 in client/src/app/Routes.tsx

View workflow job for this annotation

GitHub Actions / unit-test (18.x)

Unexpected any. Specify a different type
exact?: boolean;
routes?: undefined;
}
Expand All @@ -77,7 +85,11 @@
comp: AssessmentActions,
exact: false,
},

{
path: Paths.assessmentSummary,
comp: AssessmentSummary,
exact: false,
},
{
path: Paths.applicationsReview,
comp: Reviews,
Expand Down
3 changes: 2 additions & 1 deletion client/src/app/api/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@
binary?: string;
migrationWave: Ref | null;
assessments?: Ref[];
assessed?: boolean;
}

export interface Review {
Expand Down Expand Up @@ -204,7 +205,7 @@
identity?: Ref;
createTime?: string;
createUser?: string;
id: any;

Check warning on line 208 in client/src/app/api/models.ts

View workflow job for this annotation

GitHub Actions / unit-test (18.x)

Unexpected any. Specify a different type
enabled: boolean;
}

Expand Down Expand Up @@ -368,7 +369,7 @@

export interface TaskgroupTask {
name: string;
data: any;

Check warning on line 372 in client/src/app/api/models.ts

View workflow job for this annotation

GitHub Actions / unit-test (18.x)

Unexpected any. Specify a different type
application: Ref;
}

Expand Down Expand Up @@ -696,7 +697,7 @@
unknown: number;
yellow: number;
}
export type AssessmentStatus = "EMPTY" | "STARTED" | "COMPLETE";
export type AssessmentStatus = "empty" | "started" | "complete";
export type Risk = "GREEN" | "AMBER" | "RED" | "UNKNOWN";

export interface InitialAssessment {
Expand Down
18 changes: 14 additions & 4 deletions client/src/app/api/rest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,11 @@

type Direction = "asc" | "desc";

const buildQuery = (params: any) => {

Check warning on line 122 in client/src/app/api/rest.ts

View workflow job for this annotation

GitHub Actions / unit-test (18.x)

Unexpected any. Specify a different type
const query: string[] = [];

Object.keys(params).forEach((key) => {
const value = (params as any)[key];

Check warning on line 126 in client/src/app/api/rest.ts

View workflow job for this annotation

GitHub Actions / unit-test (18.x)

Unexpected any. Specify a different type

if (value !== undefined && value !== null) {
let queryParamValues: string[] = [];
Expand Down Expand Up @@ -237,15 +237,25 @@
.then((response) => response.data);
};

export const getAssessmentsByAppId = (
applicationId?: number | string
): Promise<Assessment[]> => {
return axios
.get(`${APPLICATIONS}/${applicationId}/assessments`)
.then((response) => response.data);
};

export const createAssessment = (
obj: InitialAssessment
): Promise<Assessment> => {
return axios.post(`${ASSESSMENTS}`, obj).then((response) => response.data);
return axios
.post(`${APPLICATIONS}/${obj?.application?.id}/assessments`, obj)
.then((response) => response.data);
};

export const patchAssessment = (obj: Assessment): AxiosPromise<Assessment> => {
export const updateAssessment = (obj: Assessment): Promise<Assessment> => {
return axios
.patch(`${ASSESSMENTS}/${obj.id}`, obj)
.put(`${ASSESSMENTS}/${obj.id}`, obj)
.then((response) => response.data);
};

Expand All @@ -268,7 +278,7 @@
}: {
queryKey: QueryKey;
}): AxiosPromise<BulkCopyAssessment> => {
const [_, id] = queryKey;

Check warning on line 281 in client/src/app/api/rest.ts

View workflow job for this annotation

GitHub Actions / unit-test (18.x)

'_' is assigned a value but never used
return APIClient.get<BulkCopyAssessment>(`${ASSESSMENTS}/bulk/${id}`);
};

Expand Down Expand Up @@ -732,7 +742,7 @@
export const getQuestionnaireById = (
id: number | string
): Promise<Questionnaire> =>
axios.get(`${QUESTIONNAIRES}/id/${id}`).then((response) => response.data);
axios.get(`${QUESTIONNAIRES}/${id}`).then((response) => response.data);

export const createQuestionnaire = (
obj: Questionnaire
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,23 @@ import spacing from "@patternfly/react-styles/css/utilities/Spacing/spacing";
import { IconedStatus } from "@app/components/IconedStatus";
import { TimesCircleIcon } from "@patternfly/react-icons";
import { WarningTriangleIcon } from "@patternfly/react-icons";

export interface IAnswerTableProps {
answers: Answer[];
hideAnswerKey?: boolean;
}

const AnswerTable: React.FC<IAnswerTableProps> = ({ answers }) => {
const AnswerTable: React.FC<IAnswerTableProps> = ({
answers,
hideAnswerKey,
}) => {
const { t } = useTranslation();

const tableControls = useLocalTableControls({
idProperty: "text",
items: answers,
items: hideAnswerKey
? answers.filter((answer) => answer.selected)
: answers,
columnNames: {
choice: "Answer choice",
weight: "Weight",
Expand Down Expand Up @@ -99,10 +106,10 @@ const AnswerTable: React.FC<IAnswerTableProps> = ({ answers }) => {
>
Tags to be applied:
</Text>
{answer?.autoAnswerFor?.map((tag: any) => {
{answer?.autoAnswerFor?.map((tag, index) => {
return (
<div style={{ flex: "0 0 6em" }}>
<Label color="grey">{tag.tag}</Label>
<div key={index} style={{ flex: "0 0 6em" }}>
<Label color="grey">{tag.tag.name}</Label>
</div>
);
})}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import React, { useState, useMemo } from "react";
import {
Tabs,
Tab,
SearchInput,
Toolbar,
ToolbarItem,
ToolbarContent,
TextContent,
PageSection,
PageSectionVariants,
Breadcrumb,
BreadcrumbItem,
Button,
Text,
} from "@patternfly/react-core";
import AngleLeftIcon from "@patternfly/react-icons/dist/esm/icons/angle-left-icon";
import { Link } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { Paths } from "@app/Paths";
import { ConditionalRender } from "@app/components/ConditionalRender";
import { AppPlaceholder } from "@app/components/AppPlaceholder";
import QuestionsTable from "@app/components/questions-table/questions-table";
import { Assessment, Questionnaire } from "@app/api/models";
import QuestionnaireSectionTabTitle from "./components/questionnaire-section-tab-title";
import { AxiosError } from "axios";
import { formatPath } from "@app/utils/utils";

export enum SummaryType {
Assessment = "Assessment",
Questionnaire = "Questionnaire",
}

interface QuestionnaireSummaryProps {
isFetching: boolean;
fetchError: AxiosError | null;
summaryData: Assessment | Questionnaire | undefined;
summaryType: SummaryType;
}

const QuestionnaireSummary: React.FC<QuestionnaireSummaryProps> = ({
summaryData,
summaryType,
isFetching,
fetchError,
}) => {
const { t } = useTranslation();

const [activeSectionIndex, setActiveSectionIndex] = useState<"all" | number>(
"all"
);

const handleTabClick = (
_event: React.MouseEvent<any> | React.KeyboardEvent | MouseEvent,
tabKey: string | number
) => {
setActiveSectionIndex(tabKey as "all" | number);
};

const [searchValue, setSearchValue] = useState("");

const filteredSummaryData = useMemo<Assessment | Questionnaire | null>(() => {
if (!summaryData) return null;

return {
...summaryData,
sections: summaryData?.sections.map((section) => ({
...section,
questions: section.questions.filter(({ text, explanation }) =>
[text, explanation].some(
(text) => text?.toLowerCase().includes(searchValue.toLowerCase())
)
),
})),
};
}, [summaryData, searchValue]);

const allQuestions =
summaryData?.sections.flatMap((section) => section.questions) || [];
const allMatchingQuestions =
filteredSummaryData?.sections.flatMap((section) => section.questions) || [];

if (!summaryData) {
return <div>No data available.</div>;
}
const BreadcrumbPath =
summaryType === SummaryType.Assessment ? (
<Breadcrumb>
<BreadcrumbItem>
<Link
to={formatPath(Paths.assessmentActions, {
applicationId: (summaryData as Assessment)?.application?.id,
})}
>
Assessment
</Link>
</BreadcrumbItem>
<BreadcrumbItem to="#" isActive>
{summaryData?.name}
</BreadcrumbItem>
</Breadcrumb>
) : (
<Breadcrumb>
<BreadcrumbItem>
<Link to={Paths.assessment}>Assessment</Link>
</BreadcrumbItem>
<BreadcrumbItem to="#" isActive>
{summaryData?.name}
</BreadcrumbItem>
</Breadcrumb>
);
return (
<>
<PageSection variant={PageSectionVariants.light}>
<TextContent>
<Text component="h1">{summaryType}</Text>
</TextContent>
{BreadcrumbPath}
</PageSection>
<PageSection>
<ConditionalRender when={isFetching} then={<AppPlaceholder />}>
<div
style={{
backgroundColor: "var(--pf-v5-global--BackgroundColor--100)",
}}
>
Comment on lines +122 to +126
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At some point we might want to see if we can get rid of this inline style somehow

<Toolbar>
<ToolbarContent>
<ToolbarItem widths={{ default: "300px" }}>
<SearchInput
placeholder="Search questions"
value={searchValue}
onChange={(_event, value) => setSearchValue(value)}
onClear={() => setSearchValue("")}
resultsCount={
(searchValue && allMatchingQuestions.length) || undefined
}
/>
</ToolbarItem>
</ToolbarContent>
</Toolbar>

<Link
to={
summaryType === SummaryType.Assessment
? formatPath(Paths.assessmentActions, {
applicationId: (summaryData as Assessment)?.application
?.id,
})
: Paths.assessment
}
>
<Button variant="link" icon={<AngleLeftIcon />}>
Back to {summaryType.toLowerCase()}
</Button>
</Link>
<div className="tabs-vertical-container">
<Tabs
activeKey={activeSectionIndex}
onSelect={handleTabClick}
isVertical
aria-label="Tabs for summaryData sections"
role="region"
>
{[
<Tab
key="all"
eventKey="all"
title={
<QuestionnaireSectionTabTitle
isSearching={!!searchValue}
sectionName="All questions"
unfilteredQuestions={allQuestions}
filteredQuestions={allMatchingQuestions}
/>
}
>
<QuestionsTable
fetchError={fetchError}
questions={allMatchingQuestions}
isSearching={!!searchValue}
data={summaryData}
isAllQuestionsTab
hideAnswerKey={summaryType === SummaryType.Assessment}
/>
</Tab>,
...(summaryData?.sections.map((section, index) => {
const filteredQuestions =
filteredSummaryData?.sections[index]?.questions || [];
return (
<Tab
key={index}
eventKey={index}
title={
<QuestionnaireSectionTabTitle
isSearching={!!searchValue}
sectionName={section.name}
unfilteredQuestions={section.questions}
filteredQuestions={filteredQuestions}
/>
}
>
<QuestionsTable
fetchError={fetchError}
questions={filteredQuestions}
isSearching={!!searchValue}
data={summaryData}
hideAnswerKey={summaryType === SummaryType.Assessment}
/>
</Tab>
);
}) || []),
]}
</Tabs>
</div>
</div>
</ConditionalRender>
</PageSection>
</>
);
};

export default QuestionnaireSummary;
Loading
Loading