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,15 +40,23 @@ const AssessmentSettings = lazy(
"./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>;
Expand Down Expand Up @@ -77,7 +85,11 @@ export const devRoutes: IRoute[] = [
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 @@ export interface Application {
binary?: string;
migrationWave: Ref | null;
assessments?: Ref[];
assessed?: boolean;
}

export interface Review {
Expand Down Expand Up @@ -696,7 +697,7 @@ export interface Thresholds {
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 @@ -237,15 +237,25 @@ export const getAssessments = (filters: {
.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 Down Expand Up @@ -732,7 +742,7 @@ export const getQuestionnaires = (): Promise<Questionnaire[]> =>
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