From 74f7406fa34a566a71ce4a23f9775b0989b999cf Mon Sep 17 00:00:00 2001 From: VineetBala-AOT <90332175+VineetBala-AOT@users.noreply.github.com> Date: Wed, 30 Aug 2023 14:29:21 -0700 Subject: [PATCH] Handle no data error for graphs (#2093) * Changes to show all survey results to superusers * removing hard coded values * fixing linting * splitting to seperate end points * fixing auth check * fixing linting * merging method in service * Handle no data error for graphs * adding new nodata component * updated * adding constant file for http error --- .../src/analytics_api/resources/aggregator.py | 2 +- .../src/analytics_api/resources/engagement.py | 4 +- .../resources/user_response_detail.py | 4 +- .../src/analytics_api/utils/roles.py | 37 -------------- .../publicDashboard/DashboardContext.tsx | 4 +- .../publicDashboard/KPI/ProjectLocation.tsx | 46 +++++++++++------ .../publicDashboard/KPI/SurveyEmailsSent.tsx | 50 +++++++++++++------ .../publicDashboard/KPI/SurveysCompleted.tsx | 50 +++++++++++++------ .../src/components/publicDashboard/NoData.tsx | 35 +++++++++++++ .../SubmissionTrend/SubmissionTrend.tsx | 29 ++++++++--- .../publicDashboard/SurveyBar/index.tsx | 34 ++++++++++--- met-web/src/constants/httpResponseCodes.ts | 3 ++ 12 files changed, 195 insertions(+), 103 deletions(-) create mode 100644 met-web/src/components/publicDashboard/NoData.tsx create mode 100644 met-web/src/constants/httpResponseCodes.ts diff --git a/analytics-api/src/analytics_api/resources/aggregator.py b/analytics-api/src/analytics_api/resources/aggregator.py index 93e245f86..0a3400b88 100644 --- a/analytics-api/src/analytics_api/resources/aggregator.py +++ b/analytics-api/src/analytics_api/resources/aggregator.py @@ -50,7 +50,7 @@ def get(): if counts: return counts, HTTPStatus.OK - return 'Engagement was not found', HTTPStatus.INTERNAL_SERVER_ERROR + return 'Engagement was not found', HTTPStatus.NOT_FOUND except KeyError: return 'Engagement was not found', HTTPStatus.INTERNAL_SERVER_ERROR except ValueError as err: diff --git a/analytics-api/src/analytics_api/resources/engagement.py b/analytics-api/src/analytics_api/resources/engagement.py index 5f4e1a1fa..8d67e3711 100644 --- a/analytics-api/src/analytics_api/resources/engagement.py +++ b/analytics-api/src/analytics_api/resources/engagement.py @@ -44,7 +44,7 @@ def get(engagement_id): if engagement_record: return engagement_record, HTTPStatus.OK - return 'Engagement was not found', HTTPStatus.INTERNAL_SERVER_ERROR + return 'Engagement was not found', HTTPStatus.NOT_FOUND except KeyError: return 'Engagement was not found', HTTPStatus.INTERNAL_SERVER_ERROR except ValueError as err: @@ -67,7 +67,7 @@ def get(engagement_id): if map_data: return map_data, HTTPStatus.OK - return 'Engagement was not found', HTTPStatus.INTERNAL_SERVER_ERROR + return 'Engagement was not found', HTTPStatus.NOT_FOUND except KeyError: return 'Engagement was not found', HTTPStatus.INTERNAL_SERVER_ERROR except ValueError as err: diff --git a/analytics-api/src/analytics_api/resources/user_response_detail.py b/analytics-api/src/analytics_api/resources/user_response_detail.py index 6487a96c4..fad9c4ec4 100644 --- a/analytics-api/src/analytics_api/resources/user_response_detail.py +++ b/analytics-api/src/analytics_api/resources/user_response_detail.py @@ -51,7 +51,7 @@ def get(engagement_id): if user_response_record: return user_response_record, HTTPStatus.OK - return 'User Response was not found', HTTPStatus.INTERNAL_SERVER_ERROR + return 'User Response was not found', HTTPStatus.NOT_FOUND except KeyError: return 'User Response was not found', HTTPStatus.INTERNAL_SERVER_ERROR except ValueError as err: @@ -80,7 +80,7 @@ def get(engagement_id): if user_response_record: return user_response_record, HTTPStatus.OK - return 'User Response was not found', HTTPStatus.INTERNAL_SERVER_ERROR + return 'User Response was not found', HTTPStatus.NOT_FOUND except KeyError: return 'User Response was not found', HTTPStatus.INTERNAL_SERVER_ERROR except ValueError as err: diff --git a/analytics-api/src/analytics_api/utils/roles.py b/analytics-api/src/analytics_api/utils/roles.py index e9a801c21..07c82a899 100644 --- a/analytics-api/src/analytics_api/utils/roles.py +++ b/analytics-api/src/analytics_api/utils/roles.py @@ -18,41 +18,4 @@ class Role(Enum): """User Role.""" - PUBLIC_USER = 'public_user' - ANONYMOUS_USER = 'anonymous_user' - - # STAFF Based roles - CREATE_TENANT = 'create_tenant' - VIEW_TENANT = 'view_tenant' - VIEW_USERS = 'view_users' - TOGGLE_USER_STATUS = 'toggle_user_status' - CREATE_ADMIN_USER = 'create_admin_user' - CREATE_TEAM = 'create_team' - CREATE_ENGAGEMENT = 'create_engagement' - VIEW_SURVEYS = 'view_surveys' - CREATE_SURVEY = 'create_survey' - EDIT_SURVEY = 'edit_survey' - CLONE_SURVEY = 'clone_survey' - PUBLISH_ENGAGEMENT = 'publish_engagement' - VIEW_ENGAGEMENT = 'view_engagement' - VIEW_ASSIGNED_ENGAGEMENTS = 'view_assigned_engagements' - VIEW_PRIVATE_ENGAGEMENTS = 'view_private_engagements' - EDIT_ENGAGEMENT = 'edit_engagement' - REVIEW_COMMENTS = 'review_comments' - REVIEW_ALL_COMMENTS = 'review_all_comments' - ACCESS_DASHBOARD = 'access_dashboard' - VIEW_MEMBERS = 'view_members' - EDIT_MEMBERS = 'edit_members' - VIEW_ALL_SURVEYS = 'view_all_surveys' # Super user can view all kind of surveys including hidden - EDIT_ALL_SURVEYS = 'edit_all_surveys' - EDIT_DRAFT_ENGAGEMENT = 'edit_draft_engagement' - EDIT_SCHEDULED_ENGAGEMENT = 'edit_scheduled_engagement' - EDIT_UPCOMING_ENGAGEMENT = 'edit_upcoming_engagement' - EDIT_OPEN_ENGAGEMENT = 'edit_open_engagement' - EDIT_CLOSED_ENGAGEMENT = 'edit_closed_engagement' - VIEW_APPROVED_COMMENTS = 'view_approved_comments' # used just in the front end to show the comment page - VIEW_FEEDBACKS = 'view_feedbacks' - VIEW_ALL_ENGAGEMENTS = 'view_all_engagements' # Allows user access to all engagements including draft - SHOW_ALL_COMMENT_STATUS = 'show_all_comment_status' # Allows user to see all comment status - EXPORT_TO_CSV = 'export_to_csv' # Allows users to export comments to csv VIEW_ALL_SURVEY_RESULTS = 'view_all_survey_results' # Allows users to view results to all questions diff --git a/met-web/src/components/publicDashboard/DashboardContext.tsx b/met-web/src/components/publicDashboard/DashboardContext.tsx index 6d3f420ad..bfaa116d2 100644 --- a/met-web/src/components/publicDashboard/DashboardContext.tsx +++ b/met-web/src/components/publicDashboard/DashboardContext.tsx @@ -60,7 +60,9 @@ export const DashboardContextProvider = ({ children }: DashboardContextProviderP /* check to ensure that users without the role access_dashboard can access the dashboard only after the engagement is closed*/ if (!isClosed && canAccessDashboard) { - throw new Error('Engagement is not yet closed'); + throw new Error( + 'The report will only be available to view after the engagement period is over and the engagement is closed.', + ); } }; diff --git a/met-web/src/components/publicDashboard/KPI/ProjectLocation.tsx b/met-web/src/components/publicDashboard/KPI/ProjectLocation.tsx index ad81982aa..cf5d30366 100644 --- a/met-web/src/components/publicDashboard/KPI/ProjectLocation.tsx +++ b/met-web/src/components/publicDashboard/KPI/ProjectLocation.tsx @@ -6,8 +6,11 @@ import { Map } from '../../../models/analytics/map'; import { Engagement } from 'models/engagement'; import { MetLabel, MetPaper } from 'components/common'; import { ErrorBox } from '../ErrorBox'; +import { NoData } from '../NoData'; import MetMap from 'components/map'; import { geoJSONDecode } from 'components/engagement/form/EngagementWidgets/Map/utils'; +import axios, { AxiosError } from 'axios'; +import { HTTP_STATUS_CODES } from 'constants/httpResponseCodes'; interface SurveysCompletedProps { engagement: Engagement; @@ -22,6 +25,12 @@ const ProjectLocation = ({ engagement, engagementIsLoading, handleProjectMapData const isTablet = useMediaQuery((theme: Theme) => theme.breakpoints.down('md')); const circleSize = isTablet ? 100 : 250; + const setErrors = (error: AxiosError) => { + if (error.response?.status !== HTTP_STATUS_CODES.NOT_FOUND) { + setIsError(true); + } + }; + const fetchData = async () => { setIsError(false); setIsLoading(true); @@ -29,9 +38,14 @@ const ProjectLocation = ({ engagement, engagementIsLoading, handleProjectMapData const response = await getMapData(Number(engagement.id)); setData(response); handleProjectMapData(response); - setIsLoading(false); } catch (error) { - setIsError(true); + if (axios.isAxiosError(error)) { + setErrors(error); + } else { + setIsError(true); + } + } finally { + setIsLoading(false); } }; @@ -41,18 +55,7 @@ const ProjectLocation = ({ engagement, engagementIsLoading, handleProjectMapData } }, [engagement.id]); - if (isError) { - return ( - { - fetchData(); - }} - /> - ); - } - - if (isLoading || engagementIsLoading || !data) { + if (isLoading || engagementIsLoading) { return ( <> Project Location @@ -74,6 +77,21 @@ const ProjectLocation = ({ engagement, engagementIsLoading, handleProjectMapData ); } + if (!data) { + return ; + } + + if (isError) { + return ( + { + fetchData(); + }} + /> + ); + } + return ( <> Project Location diff --git a/met-web/src/components/publicDashboard/KPI/SurveyEmailsSent.tsx b/met-web/src/components/publicDashboard/KPI/SurveyEmailsSent.tsx index 6e9ca9797..347d5e0da 100644 --- a/met-web/src/components/publicDashboard/KPI/SurveyEmailsSent.tsx +++ b/met-web/src/components/publicDashboard/KPI/SurveyEmailsSent.tsx @@ -3,11 +3,14 @@ import Stack from '@mui/material/Stack'; import { Box, Grid, CircularProgress, useMediaQuery, Theme } from '@mui/material'; import { DASHBOARD } from '../constants'; import { getAggregatorData } from 'services/analytics/aggregatorService'; -import { AggregatorData, createAggregatorData } from '../../../models/analytics/aggregator'; +import { AggregatorData } from '../../../models/analytics/aggregator'; import { Engagement } from 'models/engagement'; import { RadialBarChart, PolarAngleAxis, RadialBar } from 'recharts'; import { MetLabel, MetPaper } from 'components/common'; import { ErrorBox } from '../ErrorBox'; +import { NoData } from '../NoData'; +import axios, { AxiosError } from 'axios'; +import { HTTP_STATUS_CODES } from 'constants/httpResponseCodes'; interface SurveyEmailsSentProps { engagement: Engagement; @@ -15,12 +18,18 @@ interface SurveyEmailsSentProps { } const SurveyEmailsSent = ({ engagement, engagementIsLoading }: SurveyEmailsSentProps) => { - const [data, setData] = useState(createAggregatorData()); + const [data, setData] = useState(null); const [isLoading, setIsLoading] = useState(true); const [isError, setIsError] = useState(false); const isTablet = useMediaQuery((theme: Theme) => theme.breakpoints.down('md')); const circleSize = isTablet ? 100 : 250; + const setErrors = (error: AxiosError) => { + if (error.response?.status !== HTTP_STATUS_CODES.NOT_FOUND) { + setIsError(true); + } + }; + const fetchData = async () => { setIsLoading(true); setIsError(false); @@ -30,9 +39,14 @@ const SurveyEmailsSent = ({ engagement, engagementIsLoading }: SurveyEmailsSentP count_for: 'email_verification', }); setData(response); - setIsLoading(false); } catch (error) { - setIsError(true); + if (axios.isAxiosError(error)) { + setErrors(error); + } else { + setIsError(true); + } + } finally { + setIsLoading(false); } }; @@ -42,17 +56,6 @@ const SurveyEmailsSent = ({ engagement, engagementIsLoading }: SurveyEmailsSentP } }, [engagement.id]); - if (isError) { - return ( - { - fetchData(); - }} - /> - ); - } - if (isLoading || engagementIsLoading) { return ( <> @@ -79,6 +82,21 @@ const SurveyEmailsSent = ({ engagement, engagementIsLoading }: SurveyEmailsSentP ); } + if (!data) { + return ; + } + + if (isError) { + return ( + { + fetchData(); + }} + /> + ); + } + return ( <> Survey Emails Sent @@ -111,7 +129,7 @@ const SurveyEmailsSent = ({ engagement, engagementIsLoading }: SurveyEmailsSentP dominantBaseline="middle" className="progress-label" > - {data.value} + {data?.value} diff --git a/met-web/src/components/publicDashboard/KPI/SurveysCompleted.tsx b/met-web/src/components/publicDashboard/KPI/SurveysCompleted.tsx index 786cc46c7..df641474f 100644 --- a/met-web/src/components/publicDashboard/KPI/SurveysCompleted.tsx +++ b/met-web/src/components/publicDashboard/KPI/SurveysCompleted.tsx @@ -3,11 +3,14 @@ import Stack from '@mui/material/Stack'; import { Box, Grid, CircularProgress, useMediaQuery, Theme } from '@mui/material'; import { DASHBOARD } from '../constants'; import { getAggregatorData } from 'services/analytics/aggregatorService'; -import { AggregatorData, createAggregatorData } from '../../../models/analytics/aggregator'; +import { AggregatorData } from '../../../models/analytics/aggregator'; import { Engagement } from 'models/engagement'; import { RadialBarChart, PolarAngleAxis, RadialBar } from 'recharts'; import { MetLabel, MetPaper } from 'components/common'; import { ErrorBox } from '../ErrorBox'; +import { NoData } from '../NoData'; +import axios, { AxiosError } from 'axios'; +import { HTTP_STATUS_CODES } from 'constants/httpResponseCodes'; interface SurveysCompletedProps { engagement: Engagement; @@ -15,13 +18,19 @@ interface SurveysCompletedProps { } const SurveysCompleted = ({ engagement, engagementIsLoading }: SurveysCompletedProps) => { - const [data, setData] = useState(createAggregatorData()); + const [data, setData] = useState(null); const [isLoading, setIsLoading] = useState(true); const [isError, setIsError] = useState(false); const isTablet = useMediaQuery((theme: Theme) => theme.breakpoints.down('md')); const circleSize = isTablet ? 100 : 250; + const setErrors = (error: AxiosError) => { + if (error.response?.status !== HTTP_STATUS_CODES.NOT_FOUND) { + setIsError(true); + } + }; + const fetchData = async () => { setIsLoading(true); setIsError(false); @@ -31,9 +40,14 @@ const SurveysCompleted = ({ engagement, engagementIsLoading }: SurveysCompletedP count_for: 'survey_completed', }); setData(response); - setIsLoading(false); } catch (error) { - setIsError(true); + if (axios.isAxiosError(error)) { + setErrors(error); + } else { + setIsError(true); + } + } finally { + setIsLoading(false); } }; @@ -43,17 +57,6 @@ const SurveysCompleted = ({ engagement, engagementIsLoading }: SurveysCompletedP } }, [engagement.id]); - if (isError) { - return ( - { - fetchData(); - }} - /> - ); - } - if (isLoading || engagementIsLoading) { return ( <> @@ -80,6 +83,21 @@ const SurveysCompleted = ({ engagement, engagementIsLoading }: SurveysCompletedP ); } + if (!data) { + return ; + } + + if (isError) { + return ( + { + fetchData(); + }} + /> + ); + } + return ( <> Surveys Completed @@ -112,7 +130,7 @@ const SurveysCompleted = ({ engagement, engagementIsLoading }: SurveysCompletedP dominantBaseline="middle" className="progress-label" > - {data.value} + {data?.value} diff --git a/met-web/src/components/publicDashboard/NoData.tsx b/met-web/src/components/publicDashboard/NoData.tsx new file mode 100644 index 000000000..5d5cbc827 --- /dev/null +++ b/met-web/src/components/publicDashboard/NoData.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { Grid, SxProps, Theme } from '@mui/material'; +import { MetBody, MetPaper } from 'components/common'; +import { DASHBOARD } from './constants'; + +interface NoDataProps { + height?: number | string; + sx?: SxProps; +} +export const NoData = ({ sx }: NoDataProps) => { + return ( + + + + No Data Available + + + + ); +}; diff --git a/met-web/src/components/publicDashboard/SubmissionTrend/SubmissionTrend.tsx b/met-web/src/components/publicDashboard/SubmissionTrend/SubmissionTrend.tsx index 2042cc4ec..fa00875f5 100644 --- a/met-web/src/components/publicDashboard/SubmissionTrend/SubmissionTrend.tsx +++ b/met-web/src/components/publicDashboard/SubmissionTrend/SubmissionTrend.tsx @@ -4,11 +4,12 @@ import { Stack, useMediaQuery, Theme, Grid, ToggleButtonGroup, CircularProgress, import { MetPaper, MetLabel, SecondaryButton, MetToggleButton } from 'components/common'; import { DASHBOARD } from '../constants'; import { ErrorBox } from '../ErrorBox'; +import { NoData } from '../NoData'; import { getUserResponseDetailByMonth, getUserResponseDetailByWeek, } from 'services/analytics/userResponseDetailService'; -import { createDefaultByMonthData } from '../../../models/analytics/userResponseDetail'; +import { UserResponseDetailByMonth } from '../../../models/analytics/userResponseDetail'; import { Engagement } from 'models/engagement'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider, DatePicker } from '@mui/x-date-pickers'; @@ -16,6 +17,8 @@ import { Dayjs } from 'dayjs'; import { Then, If, Else, Unless } from 'react-if'; import { formatToUTC } from 'components/common/dateHelper'; import CalendarTodayIcon from '@mui/icons-material/CalendarToday'; +import axios, { AxiosError } from 'axios'; +import { HTTP_STATUS_CODES } from 'constants/httpResponseCodes'; interface SubmissionTrendProps { engagement: Engagement; @@ -38,7 +41,7 @@ const SubmissionTrend = ({ engagement, engagementIsLoading }: SubmissionTrendPro const isExtraSmall = useMediaQuery('(max-width:299px)'); const isBetweenMdAndLg = useMediaQuery((theme: Theme) => theme.breakpoints.between('lg', 'xl')); const HEIGHT = isTablet ? 200 : 250; - const [data, setData] = useState(createDefaultByMonthData()); + const [data, setData] = useState(null); const [isLoading, setIsLoading] = useState(true); const [isError, setIsError] = useState(false); const [chartBy, setChartBy] = React.useState('monthly'); @@ -50,6 +53,12 @@ const SubmissionTrend = ({ engagement, engagementIsLoading }: SubmissionTrendPro width: isExtraSmall ? '40%' : 'auto', }; + const setErrors = (error: AxiosError) => { + if (error.response?.status !== HTTP_STATUS_CODES.NOT_FOUND) { + setIsError(true); + } + }; + const fetchData = async () => { setIsLoading(true); try { @@ -68,11 +77,15 @@ const SubmissionTrend = ({ engagement, engagementIsLoading }: SubmissionTrendPro ); setData(response); } - setIsLoading(false); setIsError(false); } catch (error) { - console.log(error); - setIsError(true); + if (axios.isAxiosError(error)) { + setErrors(error); + } else { + setIsError(true); + } + } finally { + setIsLoading(false); } }; @@ -94,7 +107,7 @@ const SubmissionTrend = ({ engagement, engagementIsLoading }: SubmissionTrendPro setChartBy(chartByValue); }; - if (engagementIsLoading) { + if (isLoading || engagementIsLoading) { return ( <> Live Activity - Engagement @@ -116,6 +129,10 @@ const SubmissionTrend = ({ engagement, engagementIsLoading }: SubmissionTrendPro ); } + if (!data) { + return ; + } + if (isError) { return ; } diff --git a/met-web/src/components/publicDashboard/SurveyBar/index.tsx b/met-web/src/components/publicDashboard/SurveyBar/index.tsx index 95a82ffed..9a9d7c707 100644 --- a/met-web/src/components/publicDashboard/SurveyBar/index.tsx +++ b/met-web/src/components/publicDashboard/SurveyBar/index.tsx @@ -8,10 +8,13 @@ import { BarBlock } from './BarBlock'; import { TreemapBlock } from './TreemapBlock'; import { getSurveyResultData } from 'services/analytics/surveyResult'; import { Engagement } from 'models/engagement'; -import { SurveyResultData, createSurveyResultData, defaultData } from '../../../models/analytics/surveyResult'; +import { SurveyResultData, defaultData } from '../../../models/analytics/surveyResult'; import { ErrorBox } from '../ErrorBox'; +import { NoData } from '../NoData'; import { If, Then, Else, When } from 'react-if'; import { dashboardCustomStyles } from '../SubmissionTrend/SubmissionTrend'; +import axios, { AxiosError } from 'axios'; +import { HTTP_STATUS_CODES } from 'constants/httpResponseCodes'; const HEIGHT = 400; @@ -24,7 +27,7 @@ interface SurveyQuestionProps { export const SurveyBar = ({ readComments, engagement, engagementIsLoading, dashboardType }: SurveyQuestionProps) => { const isTablet = useMediaQuery((theme: Theme) => theme.breakpoints.down('md')); - const [data, setData] = useState(createSurveyResultData()); + const [data, setData] = useState(null); const [selectedData, setSelectedData] = useState(defaultData[0]); const [isLoading, setIsLoading] = useState(true); const [isError, setIsError] = useState(false); @@ -34,6 +37,12 @@ export const SurveyBar = ({ readComments, engagement, engagementIsLoading, dashb setChartType(chartByValue); }; + const setErrors = (error: AxiosError) => { + if (error.response?.status !== HTTP_STATUS_CODES.NOT_FOUND) { + setIsError(true); + } + }; + const fetchData = async () => { setIsLoading(true); setIsError(false); @@ -41,9 +50,14 @@ export const SurveyBar = ({ readComments, engagement, engagementIsLoading, dashb const response = await getSurveyResultData(Number(engagement.id), dashboardType); setData(response); setSelectedData(response?.data[0]); - setIsLoading(false); } catch (error) { - setIsError(true); + if (axios.isAxiosError(error)) { + setErrors(error); + } else { + setIsError(true); + } + } finally { + setIsLoading(false); } }; @@ -53,6 +67,14 @@ export const SurveyBar = ({ readComments, engagement, engagementIsLoading, dashb } }, [engagement.id]); + if (isLoading || engagementIsLoading) { + return ; + } + + if (!data) { + return ; + } + if (isError) { return ( ; - } - return ( <> diff --git a/met-web/src/constants/httpResponseCodes.ts b/met-web/src/constants/httpResponseCodes.ts new file mode 100644 index 000000000..7a5da503f --- /dev/null +++ b/met-web/src/constants/httpResponseCodes.ts @@ -0,0 +1,3 @@ +export const HTTP_STATUS_CODES = { + NOT_FOUND: 404, +};