diff --git a/.github/workflows/ui.yml b/.github/workflows/ui.yml
index 8133ab0..b31e709 100644
--- a/.github/workflows/ui.yml
+++ b/.github/workflows/ui.yml
@@ -53,7 +53,6 @@ jobs:
run: yarn build:${{ inputs.environment }}
- name: Get AWS Creds
- if: github.actor != 'dependabot[bot]'
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.ROLEARN }}
@@ -61,5 +60,4 @@ jobs:
aws-region: us-east-1
- name: AWS Sync
- if: github.actor != 'dependabot[bot]'
run: aws s3 sync ./dist s3://${{ secrets.BUCKET_NAME }}/ --delete
diff --git a/src/components/ConfirmDialog/ConfirmDialog.tsx b/src/components/ConfirmDialog/ConfirmDialog.tsx
new file mode 100644
index 0000000..a2ed0dc
--- /dev/null
+++ b/src/components/ConfirmDialog/ConfirmDialog.tsx
@@ -0,0 +1,46 @@
+import Dialog from '@mui/material/Dialog'
+import DialogActions from '@mui/material/DialogActions'
+import DialogContent from '@mui/material/DialogContent'
+import DialogTitle from '@mui/material/DialogTitle'
+import { Box, IconButton, Typography, Button } from '@mui/material'
+import { CONFIRMATION_MESSAGE } from '@/constants'
+import { Button as CmsButton } from '@cmsgov/design-system'
+
+import CloseIcon from '@mui/icons-material/Close'
+type ConfirmDialogTypes = {
+ open: boolean
+ onClose: () => void
+ confirmClick: (confirm: boolean) => void
+}
+
+const ConfirmDialog = ({ open, onClose, confirmClick }: ConfirmDialogTypes) => {
+ const handleConfirm = () => {
+ confirmClick(true)
+ onClose()
+ }
+ const handleClose = () => {
+ confirmClick(false)
+ onClose()
+ }
+ return (
+
+ )
+}
+
+export default ConfirmDialog
diff --git a/src/components/DialogTitle/CustomDialogTitle.tsx b/src/components/DialogTitle/CustomDialogTitle.tsx
new file mode 100644
index 0000000..fe4ff63
--- /dev/null
+++ b/src/components/DialogTitle/CustomDialogTitle.tsx
@@ -0,0 +1,20 @@
+import * as React from 'react'
+import DialogTitle from '@mui/material/DialogTitle'
+import Typography from '@mui/material/Typography'
+/**
+ * Component that renders the title of the dialog.
+ * @param {string} title - The title to be displayed.
+ * @returns {JSX.Element} Component that renders the title of the dialog.
+ */
+type CustomDialogTitleProps = {
+ title: string
+}
+export default function CustomDialogTitle({ title }: CustomDialogTitleProps) {
+ return (
+
+
+ {title}
+
+
+ )
+}
diff --git a/src/components/ErrorBoundary.test.tsx b/src/components/ErrorBoundary.test.tsx
index 1967527..3bc0824 100644
--- a/src/components/ErrorBoundary.test.tsx
+++ b/src/components/ErrorBoundary.test.tsx
@@ -28,27 +28,6 @@ afterEach(() => {
jest.clearAllMocks()
})
-test('renders auth session error and navigate to logout after 5 seconds', () => {
- const navigateMock = jest.fn().mockImplementation(() => ({}))
- useNavigateMock.mockReturnValue(navigateMock)
- isRouteErrorResponseMock.mockReturnValue(true)
- routeErrorMock.mockReturnValue({
- status: 401,
- statusText: 'Unauthorized',
- data: 'Unauthorized',
- })
-
- render()
-
- expect(
- screen.getByText(/Your session has expired. Please login again./i)
- ).toBeInTheDocument()
-
- jest.advanceTimersByTime(5000)
-
- expect(navigateMock).toHaveBeenCalledWith(Routes.AUTH_LOGOUT)
-})
-
test('renders generic error message', () => {
routeErrorMock.mockReturnValue(new Error('Something went wrong'))
render()
diff --git a/src/constants.ts b/src/constants.ts
index 6648918..f35755e 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -2,6 +2,7 @@
* Application-wide constants
* @module constants
*/
+import { userData } from './types'
//* Application Strings
export const ORG_NAME = 'CMS'
@@ -21,3 +22,71 @@ export const ERROR_MESSAGES = {
error:
'An error occurred. Please log in and try again. If the error persists, please contact support.',
}
+export const EMPTY_USER: userData = {
+ userid: '',
+ email: '',
+ fullname: '',
+ role: '',
+ assignedfismasystems: [],
+}
+export const CONFIRMATION_MESSAGE =
+ 'Your changes will not be saved! Are you sure you want to close out of editing a Fisma system before saving your changes?'
+
+export const PILLAR_FUNCTION_MAP: { [key: string]: string[] } = {
+ Identity: [
+ 'AccessManagement',
+ 'Identity-AutomationOrchestration',
+ 'Identity-Governance',
+ 'IdentityStores-Users',
+ 'RiskAssessment',
+ 'Authentication-Users',
+ 'Identity-VisibilityAnalytics',
+ ],
+ Devices: [
+ 'AssetRiskManagement',
+ 'Device-AutomationOrchestration',
+ 'Device-Governance',
+ 'DeviceThreatProtection',
+ 'PolicyEnforcement',
+ 'Device-VisibilityAnalytics',
+ 'ResourceAccess',
+ ],
+ Networks: [
+ 'Network-AutomationOrchestration',
+ 'Network-Encryption',
+ 'Network-Governance',
+ 'NetworkResilience',
+ 'NetworkSegmentation',
+ 'NetworkTrafficManagement',
+ 'Network-VisibilityAnalytics',
+ ],
+ Applications: [
+ 'AccessibleApplications',
+ 'AccessAuthorization-Users',
+ 'Application-AutomationOrchestration',
+ 'Application-Governance',
+ 'SecureDevDeployWorkflow',
+ 'ApplicationSecurityTesting',
+ 'AppThreatProtection',
+ 'Application-VisibilityAnalytics',
+ ],
+ Data: [
+ 'DataAccess',
+ 'Data-AutomationOrchestration',
+ 'DataAvailability',
+ 'DataCategorization',
+ 'DataEncryption',
+ 'Data-Governance',
+ 'DataInventoryManagement',
+ 'Data-VisibilityAnalytics',
+ ],
+ CrossCutting: [
+ 'Cross-AutomationOrchestration',
+ 'Cross-Governance',
+ 'Cross-VisibilityAnalytics',
+ ],
+}
+export const TEXTFIELD_HELPER_TEXT = 'This field is required'
+
+export const INVALID_INPUT_TEXT = (key: string) =>
+ `Please provide a valid ${key}`
diff --git a/src/types.ts b/src/types.ts
index d7e0f1d..4c35dd2 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -34,7 +34,7 @@ export type RequestOptions = {
}
export type FismaSystemType = {
fismasystemid: number
- fismauid: string | number
+ fismauid: string
fismaacronym: string
fismaname: string
fismasubsystem: string
@@ -43,6 +43,10 @@ export type FismaSystemType = {
fismaimpactlevel: string
issoemail: string
datacenterenvironment: string
+ datacallcontact?: string
+ groupacronym?: string
+ groupname?: string
+ divisionname?: string
}
export type FismaSystems = {
fismaSystems: FismaSystemType[]
@@ -89,6 +93,14 @@ export type SystemDetailsModalProps = {
onClose: () => void
system: FismaSystemType | null
}
+export type editSystemModalProps = {
+ title: string
+ open: boolean
+ onClose: (data: FismaSystemType) => void
+ system: FismaSystemType | null
+ mode: string
+}
+
export type ScoreData = {
datacallid: number
fismasystemid: number
@@ -104,6 +116,26 @@ export type users = {
isNew?: boolean
}
+export type datacall = {
+ datacallid: number
+ datacall: string
+ datecreated: number
+ deadline: number
+}
+
+export type FormValidType = {
+ [key: string]: boolean
+}
+
+export type FormValidHelperText = {
+ [key: string]: string
+}
+
+export type FismaTableProps = {
+ scores: Record
+ latestDataCallId: number
+}
+
export type ThemeColor =
| 'primary'
| 'secondary'
diff --git a/src/views/EditSystemModal/EditSystemModal.tsx b/src/views/EditSystemModal/EditSystemModal.tsx
new file mode 100644
index 0000000..b602ba7
--- /dev/null
+++ b/src/views/EditSystemModal/EditSystemModal.tsx
@@ -0,0 +1,550 @@
+import * as React from 'react'
+import TextField from '@mui/material/TextField'
+import Dialog from '@mui/material/Dialog'
+import DialogActions from '@mui/material/DialogActions'
+import DialogContent from '@mui/material/DialogContent'
+import CustomDialogTitle from '../../components/DialogTitle/CustomDialogTitle'
+import { Button as CmsButton } from '@cmsgov/design-system'
+import { Box, Grid } from '@mui/material'
+import {
+ editSystemModalProps,
+ FismaSystemType,
+ FormValidType,
+ FormValidHelperText,
+} from '@/types'
+import MenuItem from '@mui/material/MenuItem'
+import ValidatedTextField from './ValidatedTextField'
+import { emailValidator } from './validators'
+import { EMPTY_SYSTEM } from './emptySystem'
+import { datacenterenvironment } from './dataEnvironment'
+import CircularProgress from '@mui/material/CircularProgress'
+import ConfirmDialog from '@/components/ConfirmDialog/ConfirmDialog'
+import _ from 'lodash'
+import axiosInstance from '@/axiosConfig'
+import { useNavigate } from 'react-router-dom'
+import { Routes } from '@/router/constants'
+import {
+ ERROR_MESSAGES,
+ TEXTFIELD_HELPER_TEXT,
+ INVALID_INPUT_TEXT,
+} from '@/constants'
+import { useSnackbar } from 'notistack'
+/**
+ * Component that renders a modal to edit fisma systems.
+ * @param {boolean, function, FismaSystemType} editSystemModalProps - props to get populate dialog and function .
+ * @returns {JSX.Element} Component that renders a dialog to edit details of a fisma systems.
+ */
+
+export default function EditSystemModal({
+ title,
+ open,
+ onClose,
+ system,
+ mode,
+}: editSystemModalProps) {
+ const [formValid, setFormValid] = React.useState({
+ issoemail: false,
+ datacallcontact: false,
+ fismaname: false,
+ fismaacronym: false,
+ datacenterenvironment: false,
+ component: false,
+ fismauid: false,
+ })
+ const isFormValid = (): boolean => {
+ return Object.values(formValid).every((value) => value === true)
+ }
+ const navigate = useNavigate()
+ const { enqueueSnackbar } = useSnackbar()
+ const [loading, setLoading] = React.useState(true)
+ const [openAlert, setOpenAlert] = React.useState(false)
+ const [formValidErrorText, setFormValidErrorText] =
+ React.useState({
+ issoemail: TEXTFIELD_HELPER_TEXT,
+ datacallcontact: TEXTFIELD_HELPER_TEXT,
+ fismaname: TEXTFIELD_HELPER_TEXT,
+ fismaacronym: TEXTFIELD_HELPER_TEXT,
+ datacenterenvironment: TEXTFIELD_HELPER_TEXT,
+ component: TEXTFIELD_HELPER_TEXT,
+ fismauid: TEXTFIELD_HELPER_TEXT,
+ })
+ const handleConfirmReturn = (confirm: boolean) => {
+ if (confirm) {
+ onClose(EMPTY_SYSTEM)
+ }
+ }
+ const handleInputChange = (
+ e: React.ChangeEvent,
+ key: string
+ ) => {
+ const value = e.target.value
+ const isValid = value.length > 0
+
+ setEditedFismaSystem((prevState) => ({
+ ...prevState,
+ [key]: value,
+ }))
+ setFormValid((prevState) => ({
+ ...prevState,
+ [key]: isValid,
+ }))
+ if (!isValid) {
+ setFormValidErrorText((prevState) => ({
+ ...prevState,
+ [key]: isValid ? '' : TEXTFIELD_HELPER_TEXT,
+ }))
+ }
+ }
+ const [editedFismaSystem, setEditedFismaSystem] =
+ React.useState(EMPTY_SYSTEM)
+ React.useEffect(() => {
+ if (system && open) {
+ setFormValid((prevState) => ({
+ ...prevState,
+ issoemail:
+ system?.issoemail && system?.issoemail.length > 0 ? true : false,
+ datacallcontact:
+ system?.datacallcontact && system?.datacallcontact.length > 0
+ ? true
+ : false,
+ fismaname:
+ system?.fismaname && system?.fismaname.length > 0 ? true : false,
+ fismaacronym:
+ system?.fismaacronym && system?.fismaacronym.length > 0
+ ? true
+ : false,
+ datacenterenvironment:
+ system?.datacenterenvironment &&
+ system?.datacenterenvironment.length > 0
+ ? true
+ : false,
+ component:
+ system?.component && system?.component.length > 0 ? true : false,
+ fismauid:
+ system?.fismauid && system?.fismauid.length > 0 ? true : false,
+ }))
+ setEditedFismaSystem(system)
+ setLoading(false)
+ }
+ }, [system, open])
+ const handleClose = () => {
+ if (_.isEqual(system, editedFismaSystem)) {
+ onClose(editedFismaSystem)
+ } else {
+ setOpenAlert(true)
+ }
+ return
+ }
+ const handleSave = async () => {
+ if (mode === 'edit') {
+ await axiosInstance
+ .put(`fismasystems/${editedFismaSystem.fismasystemid}`, {
+ fismauid: editedFismaSystem.fismauid,
+ fismaacronym: editedFismaSystem.fismaacronym,
+ fismaname: editedFismaSystem.fismaname,
+ fismasubsystem: editedFismaSystem.fismasubsystem,
+ component: editedFismaSystem.component,
+ groupacronym: editedFismaSystem.groupacronym,
+ groupname: editedFismaSystem.groupname,
+ divisionname: editedFismaSystem.divisionname,
+ datacenterenvironment: editedFismaSystem.datacenterenvironment,
+ datacallcontact: editedFismaSystem.datacallcontact,
+ issoemail: editedFismaSystem.issoemail,
+ })
+ .then((res) => {
+ if (res.status !== 200 && res.status.toString()[0] === '4') {
+ navigate(Routes.SIGNIN, {
+ replace: true,
+ state: {
+ message: ERROR_MESSAGES.expired,
+ },
+ })
+ }
+ enqueueSnackbar(`Saved`, {
+ variant: 'success',
+ anchorOrigin: {
+ vertical: 'top',
+ horizontal: 'left',
+ },
+ autoHideDuration: 1500,
+ })
+ onClose(editedFismaSystem)
+ })
+ .catch((error) => {
+ if (error.response.status === 400) {
+ const data: { [key: string]: string } = error.response.data.data
+ Object.entries(data).forEach(([key]) => {
+ // formValid.current[key] = false
+ setFormValid((prevState) => ({
+ ...prevState,
+ [key]: false,
+ }))
+ setFormValidErrorText((prevState) => ({
+ ...prevState,
+ [key]: INVALID_INPUT_TEXT(key),
+ }))
+ })
+ enqueueSnackbar(`Not Saved`, {
+ variant: 'error',
+ anchorOrigin: {
+ vertical: 'top',
+ horizontal: 'left',
+ },
+ autoHideDuration: 1500,
+ })
+ } else {
+ navigate(Routes.SIGNIN, {
+ replace: true,
+ state: {
+ message: ERROR_MESSAGES.error,
+ },
+ })
+ }
+ })
+ } else if (mode === 'create') {
+ await axiosInstance
+ .post(`fismasystems`, {
+ fismauid: editedFismaSystem.fismauid,
+ fismaacronym: editedFismaSystem.fismaacronym,
+ fismaname: editedFismaSystem.fismaname,
+ fismasubsystem: editedFismaSystem.fismasubsystem,
+ component: editedFismaSystem.component,
+ groupacronym: editedFismaSystem.groupacronym,
+ groupname: editedFismaSystem.groupname,
+ divisionname: editedFismaSystem.divisionname,
+ datacenterenvironment: editedFismaSystem.datacenterenvironment,
+ datacallcontact: editedFismaSystem.datacallcontact,
+ issoemail: editedFismaSystem.issoemail,
+ })
+ .then((res) => {
+ if (res.status !== 200 && res.status.toString()[0] === '4') {
+ navigate(Routes.SIGNIN, {
+ replace: true,
+ state: {
+ message: ERROR_MESSAGES.expired,
+ },
+ })
+ }
+ enqueueSnackbar(`Created`, {
+ variant: 'success',
+ anchorOrigin: {
+ vertical: 'top',
+ horizontal: 'left',
+ },
+ autoHideDuration: 1500,
+ })
+ onClose(editedFismaSystem)
+ })
+ .catch((error) => {
+ if (error.response.status === 400) {
+ const data: { [key: string]: string } = error.response.data.data
+ Object.entries(data).forEach(([key]) => {
+ // formValid.current[key] = false
+ setFormValid((prevState) => ({
+ ...prevState,
+ [key]: false,
+ }))
+ setFormValidErrorText((prevState) => ({
+ ...prevState,
+ [key]: INVALID_INPUT_TEXT(key),
+ }))
+ })
+ enqueueSnackbar(`Not Created`, {
+ variant: 'error',
+ anchorOrigin: {
+ vertical: 'top',
+ horizontal: 'left',
+ },
+ autoHideDuration: 1500,
+ })
+ } else {
+ navigate(Routes.SIGNIN, {
+ replace: true,
+ state: {
+ message: ERROR_MESSAGES.error,
+ },
+ })
+ }
+ })
+ }
+ }
+ if (open && system) {
+ if (loading) {
+ return (
+
+
+
+ )
+ }
+ return (
+ <>
+
+ setOpenAlert(false)}
+ confirmClick={handleConfirmReturn}
+ />
+ >
+ )
+ }
+ return <>>
+}
diff --git a/src/views/EditSystemModal/ValidatedTextField.tsx b/src/views/EditSystemModal/ValidatedTextField.tsx
new file mode 100644
index 0000000..131f39d
--- /dev/null
+++ b/src/views/EditSystemModal/ValidatedTextField.tsx
@@ -0,0 +1,57 @@
+import { useState, ChangeEvent } from 'react'
+import { TextField } from '@mui/material'
+import { useEffect, useRef } from 'react'
+type ValidatedTextFieldProps = {
+ label: string
+ dfValue?: string
+ isFullWidth?: boolean
+ validator: (value: string) => string | false
+ onChange: (isValid: boolean, value: string) => void
+}
+const ValidatedTextField: React.FC = ({
+ label,
+ validator,
+ isFullWidth,
+ dfValue,
+ onChange,
+}) => {
+ const [value, setValue] = useState(dfValue || '')
+ const [error, setError] = useState(false)
+ const isInitialMount = useRef(true)
+
+ useEffect(() => {
+ if (isInitialMount.current) {
+ isInitialMount.current = false
+ const errorMessage = validator(dfValue || '')
+ setError(errorMessage)
+ onChange(!errorMessage, dfValue || '')
+ }
+ }, [dfValue, validator, onChange])
+ const handleChange = (e: ChangeEvent) => {
+ const newValue = e.target.value
+ const errorMessage = validator(newValue)
+ setValue(newValue)
+ setError(errorMessage)
+ onChange(!errorMessage, newValue)
+ }
+
+ return (
+
+ )
+}
+export default ValidatedTextField
diff --git a/src/views/EditSystemModal/dataEnvironment.ts b/src/views/EditSystemModal/dataEnvironment.ts
new file mode 100644
index 0000000..8333ee0
--- /dev/null
+++ b/src/views/EditSystemModal/dataEnvironment.ts
@@ -0,0 +1,39 @@
+/**
+ * datacenterenvironment values for the select dropdown
+ * to edit a system
+ */
+
+export const datacenterenvironment = [
+ {
+ value: 'AWS',
+ label: 'AWS',
+ },
+ {
+ value: 'CMS-Cloud-AWS',
+ label: 'CMS-Cloud-AWS',
+ },
+ {
+ value: 'CMSDC',
+ label: 'CMSDC',
+ },
+ {
+ value: 'CMS-Cloud-MAG',
+ label: 'CMS-Cloud-MAG',
+ },
+ {
+ value: 'DECOMMISSIONED',
+ label: 'DECOMMISSIONED',
+ },
+ {
+ value: 'OPDC',
+ label: 'OPDC',
+ },
+ {
+ value: 'Other',
+ label: 'Other',
+ },
+ {
+ value: 'SaaS',
+ label: 'SaaS',
+ },
+]
diff --git a/src/views/EditSystemModal/emptySystem.ts b/src/views/EditSystemModal/emptySystem.ts
new file mode 100644
index 0000000..c3c7713
--- /dev/null
+++ b/src/views/EditSystemModal/emptySystem.ts
@@ -0,0 +1,16 @@
+export const EMPTY_SYSTEM = {
+ fismauid: '',
+ fismaacronym: '',
+ fismaname: '',
+ fismasubsystem: '',
+ fismaimpactlevel: '',
+ mission: '',
+ fismasystemid: 0,
+ component: '',
+ groupacronym: '',
+ groupname: '',
+ divisionname: '',
+ datacenterenvironment: '',
+ datacallcontact: '',
+ issoemail: '',
+}
diff --git a/src/views/EditSystemModal/validators.ts b/src/views/EditSystemModal/validators.ts
new file mode 100644
index 0000000..f86ea3e
--- /dev/null
+++ b/src/views/EditSystemModal/validators.ts
@@ -0,0 +1,8 @@
+export const emailValidator = (value: string): string | false => {
+ if (value.length === 0) {
+ return 'This field is required'
+ }
+ if (!/^[a-zA-Z0-9._:$!%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]+$/.test(value))
+ return 'Invalid email address'
+ return false
+}
diff --git a/src/views/FismaTable/FismaTable.tsx b/src/views/FismaTable/FismaTable.tsx
index 7749a28..2c917bc 100644
--- a/src/views/FismaTable/FismaTable.tsx
+++ b/src/views/FismaTable/FismaTable.tsx
@@ -4,34 +4,43 @@ import {
GridColDef,
GridFooterContainer,
GridSlotsComponentsProps,
+ GridRenderCellParams,
+ GridActionsCellItem,
GridFooter,
GridRowId,
+ useGridApiRef,
} from '@mui/x-data-grid'
import Tooltip from '@mui/material/Tooltip'
import { Box, IconButton } from '@mui/material'
import { useState } from 'react'
import FileDownloadSharpIcon from '@mui/icons-material/FileDownloadSharp'
-import Link from '@mui/material/Link'
import QuestionnareModal from '../QuestionnareModal/QuestionnareModal'
+import EditSystemModal from '../EditSystemModal/EditSystemModal'
import CustomSnackbar from '../Snackbar/Snackbar'
import axiosInstance from '@/axiosConfig'
-
-type FismaTable2Props = {
- fismaSystems: FismaSystemType[]
- scores: Record
-}
+import { useContextProp } from '../Title/Context'
+import { EMPTY_USER } from '../../constants'
+import { useNavigate } from 'react-router-dom'
+import { Routes } from '@/router/constants'
+import { ERROR_MESSAGES } from '../../constants'
+import EditIcon from '@mui/icons-material/Edit'
+import QuestionAnswerOutlinedIcon from '@mui/icons-material/QuestionAnswerOutlined'
+import { FismaTableProps } from '@/types'
type selectedRowsType = GridRowId[]
declare module '@mui/x-data-grid' {
interface FooterPropsOverrides {
selectedRows: selectedRowsType
fismaSystems: FismaSystemType[]
+ latestDataCallId: number
}
}
+
export function CustomFooterSaveComponent(
props: NonNullable
) {
const [openSnackbar, setOpenSnackbar] = useState(false)
+ const navigate = useNavigate()
const handleCloseSnackbar = () => {
setOpenSnackbar(false)
}
@@ -39,7 +48,7 @@ export function CustomFooterSaveComponent(
if (props.selectedRows && props.selectedRows.length === 0) {
setOpenSnackbar(true)
} else {
- let exportUrl = '/datacalls/2/export'
+ let exportUrl = `/datacalls/${props.latestDataCallId}/export`
if (
props.selectedRows &&
props.fismaSystems &&
@@ -63,23 +72,33 @@ export function CustomFooterSaveComponent(
})
.then((response) => {
if (response.status !== 200) {
- console.log('Error saving systems')
- } else {
- const [, filename] =
- response.headers['content-disposition'].split('filename=')
- const contentType = response.headers['content-type']
- const data = new Blob([response.data], { type: contentType })
- const url = window.URL.createObjectURL(data)
- const tempLink = document.createElement('a')
- tempLink.href = url
- tempLink.setAttribute('download', filename)
- tempLink.setAttribute('target', '_blank')
- tempLink.click()
- window.URL.revokeObjectURL(url)
+ navigate(Routes.SIGNIN, {
+ replace: true,
+ state: {
+ message: ERROR_MESSAGES.expired,
+ },
+ })
}
+ const [, filename] =
+ response.headers['content-disposition'].split('filename=')
+ const contentType = response.headers['content-type']
+ const data = new Blob([response.data], { type: contentType })
+ const url = window.URL.createObjectURL(data)
+ const tempLink = document.createElement('a')
+ tempLink.href = url
+ tempLink.setAttribute('download', filename)
+ tempLink.setAttribute('target', '_blank')
+ tempLink.click()
+ window.URL.revokeObjectURL(url)
})
.catch((error) => {
console.error('Error saving system answers: ', error)
+ navigate(Routes.SIGNIN, {
+ replace: true,
+ state: {
+ message: ERROR_MESSAGES.error,
+ },
+ })
})
}
}
@@ -112,10 +131,17 @@ export function CustomFooterSaveComponent(
>
)
}
-export default function FismaTable({ fismaSystems, scores }: FismaTable2Props) {
+export default function FismaTable({
+ scores,
+ latestDataCallId,
+}: FismaTableProps) {
+ const apiRef = useGridApiRef()
+ const { fismaSystems } = useContextProp()
const [open, setOpen] = useState(false)
+ const { userInfo } = useContextProp() || EMPTY_USER
const [selectedRow, setSelectedRow] = useState(null)
const [selectedRows, setSelectedRows] = useState([])
+ const [openEditModal, setOpenEditModal] = useState(false)
const handleOpenModal = (row: FismaSystemType) => {
setSelectedRow(row)
setOpen(true)
@@ -124,6 +150,24 @@ export default function FismaTable({ fismaSystems, scores }: FismaTable2Props) {
setOpen(false)
setSelectedRow(null)
}
+ const handleEditOpenModal = (
+ event: React.MouseEvent,
+ row: FismaSystemType
+ ) => {
+ event.stopPropagation()
+ setSelectedRow(row)
+ setOpenEditModal(true)
+ }
+ const handleCloseEditModal = (newRowData: FismaSystemType) => {
+ if (selectedRow) {
+ const row = apiRef.current.getRow(selectedRow?.fismasystemid)
+ if (row) {
+ apiRef.current.updateRows([newRowData])
+ }
+ }
+ setOpenEditModal(false)
+ setSelectedRow(null)
+ }
const columns: GridColDef[] = [
{
field: 'fismaname',
@@ -146,8 +190,12 @@ export default function FismaTable({ fismaSystems, scores }: FismaTable2Props) {
renderCell: (params) => {
const name = params.row.issoemail.split('@')
const fullName = name[0].replace(/[0-9]/g, '').split('.')
- const firstName = fullName[0][0].toUpperCase() + fullName[0].slice(1)
- const lastName = fullName[1][0].toUpperCase() + fullName[1].slice(1)
+ let firstName = ''
+ let lastName = ''
+ if (fullName.length > 1) {
+ firstName = fullName[0][0].toUpperCase() + fullName[0].slice(1)
+ lastName = fullName[1][0].toUpperCase() + fullName[1].slice(1)
+ }
return fullName.length > 1 ? `${firstName} ${lastName}` : fullName[0]
},
},
@@ -206,18 +254,35 @@ export default function FismaTable({ fismaSystems, scores }: FismaTable2Props) {
headerName: 'Actions',
headerAlign: 'center',
align: 'center',
- flex: 1,
+ flex: 0.5,
hideable: false,
sortable: false,
disableColumnMenu: true,
- renderCell: (params) => (
- handleOpenModal(params.row as FismaSystemType)}
- >
- View Questionnare
-
+ renderCell: (params: GridRenderCellParams) => (
+ <>
+
+ }
+ key={`question-${params.row.fismasystemid}`}
+ label="View Questionnare"
+ className="textPrimary"
+ onClick={() => handleOpenModal(params.row as FismaSystemType)}
+ color="inherit"
+ />
+
+ {userInfo.role === 'ADMIN' && (
+ }
+ key={`edit-${params.row.fismasystemid}`}
+ label="Edit"
+ className="textPrimary"
+ onClick={(event) =>
+ handleEditOpenModal(event, params.row as FismaSystemType)
+ }
+ color="inherit"
+ />
+ )}
+ >
),
},
]
@@ -228,13 +293,14 @@ export default function FismaTable({ fismaSystems, scores }: FismaTable2Props) {
rows={fismaSystems}
columns={columns}
checkboxSelection
+ apiRef={apiRef}
getRowId={(row) => row.fismasystemid}
onRowSelectionModelChange={(ids) => {
const selectedIDs = Array.from(ids)
setSelectedRows(selectedIDs)
}}
slotProps={{
- footer: { selectedRows, fismaSystems },
+ footer: { selectedRows, fismaSystems, latestDataCallId },
filterPanel: {
sx: {
'& .MuiFormLabel-root': {
@@ -280,6 +346,13 @@ export default function FismaTable({ fismaSystems, scores }: FismaTable2Props) {
onClose={handleCloseModal}
system={selectedRow}
/>
+
)
}
diff --git a/src/views/Home/Home.tsx b/src/views/Home/Home.tsx
index 560e2df..52eff55 100644
--- a/src/views/Home/Home.tsx
+++ b/src/views/Home/Home.tsx
@@ -16,6 +16,7 @@ export default function HomePageContainer() {
const navigate = useNavigate()
const [fismaSystems, setFismaSystems] = useState([])
const [scoreMap, setScoreMap] = useState>({})
+ const [latestDataCallId, setLatestDataCallId] = useState(0)
useEffect(() => {
async function fetchFismaSystems() {
try {
@@ -81,14 +82,40 @@ export default function HomePageContainer() {
}
fetchScores()
}, [navigate])
+ useEffect(() => {
+ async function fetchLatestDatacall() {
+ try {
+ axiosInstance.get('/datacalls').then((res) => {
+ if (res.status !== 200 && res.status.toString()[0] === '4') {
+ navigate(Routes.SIGNIN, {
+ replace: true,
+ state: {
+ message: ERROR_MESSAGES.expired,
+ },
+ })
+ }
+ setLatestDataCallId(res.data.data[0].datacallid)
+ })
+ } catch (error) {
+ console.error(error)
+ navigate(Routes.SIGNIN, {
+ replace: true,
+ state: {
+ message: ERROR_MESSAGES.error,
+ },
+ })
+ }
+ }
+ fetchLatestDatacall()
+ }, [navigate])
if (loading) {
return Loading...
}
return (
<>
-
-
+
+
>
)
diff --git a/src/views/QuestionnareModal/QuestionnareModal.tsx b/src/views/QuestionnareModal/QuestionnareModal.tsx
index 94ecda0..99936c5 100644
--- a/src/views/QuestionnareModal/QuestionnareModal.tsx
+++ b/src/views/QuestionnareModal/QuestionnareModal.tsx
@@ -30,7 +30,7 @@ import { Routes } from '@/router/constants'
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'
import IconButton from '@mui/material/IconButton'
import Tooltip from '@mui/material/Tooltip'
-import { ERROR_MESSAGES } from '@/constants'
+import { ERROR_MESSAGES, PILLAR_FUNCTION_MAP } from '@/constants'
const CssTextField = styled(TextField)({
'& label.Mui-focused': {
color: 'rgb(13, 36, 153)',
@@ -265,13 +265,7 @@ export default function QuestionnareModal({
}
return res.data.data
})
- let latestDataCallId = -Infinity
- for (let i = 0; i < datacall.length; i++) {
- latestDataCallId = Math.max(
- latestDataCallId,
- datacall[i].datacallid
- )
- }
+ const latestDataCallId = datacall[0].datacallid
setDatacallID(latestDataCallId)
await axiosInstance
.get(`/fismasystems/${system.fismasystemid}/questions`)
@@ -300,13 +294,31 @@ export default function QuestionnareModal({
const sortedPillars = Object.keys(organizedData).sort(
(a, b) => pillarOrder[a] - pillarOrder[b]
)
+ const sortSteps = (
+ steps: FismaQuestion[],
+ order: string[]
+ ): FismaQuestion[] => {
+ return steps.sort(
+ (a, b) =>
+ order.indexOf(a.function.function) -
+ order.indexOf(b.function.function)
+ )
+ }
const categoriesData: Category[] = sortedPillars.map(
(pillar) => ({
name: pillar,
- steps: organizedData[pillar],
+ steps: sortSteps(
+ organizedData[pillar],
+ PILLAR_FUNCTION_MAP[pillar]
+ ),
})
)
-
+ // const categoriesData: Category[] = sortedPillars.map(
+ // (pillar) => ({
+ // name: pillar,
+ // steps: organizedData[pillar],
+ // })
+ // )
setCategories(categoriesData)
if (data.length > 0) {
setQuestionId(categoriesData[0]['steps'][0].function.functionid)
diff --git a/src/views/QuestionnareModal/__test__/QuestionnareModal.test.tsx b/src/views/QuestionnareModal/__test__/QuestionnareModal.test.tsx
new file mode 100644
index 0000000..55e9681
--- /dev/null
+++ b/src/views/QuestionnareModal/__test__/QuestionnareModal.test.tsx
@@ -0,0 +1,26 @@
+import mockAxios from 'axios'
+jest.mock('axios')
+const mAxios = mockAxios as jest.Mocked
+
+const mockResponse = [
+ {
+ function: {
+ datacenterenvironment: 'test-center-environment',
+ description: 'test description',
+ function: 'test function',
+ functiondid: 1,
+ },
+ pillar: {
+ order: 1,
+ pillar: 'test pillar',
+ pillarid: 1,
+ },
+ noteprompt: 'test note prompt',
+ order: 'test-order none',
+ question: 'test question',
+ questionid: 'test-question-id',
+ },
+]
+it('test axios fetch', async () => {
+ mAxios.get.mockResolvedValueOnce({ data: mockResponse })
+})
diff --git a/src/views/StatisticBlocks/StatisticsBlocks.tsx b/src/views/StatisticBlocks/StatisticsBlocks.tsx
index 2a90cf0..416c8c8 100644
--- a/src/views/StatisticBlocks/StatisticsBlocks.tsx
+++ b/src/views/StatisticBlocks/StatisticsBlocks.tsx
@@ -3,7 +3,7 @@ import Box from '@mui/material/Box'
import Paper from '@mui/material/Paper'
import { Typography } from '@mui/material'
import { styled } from '@mui/material/styles'
-import { FismaSystemType } from '@/types'
+import { useContextProp } from '../Title/Context'
const StatisticsPaper = styled(Paper)(({ theme }) => ({
width: 120,
height: 120,
@@ -14,12 +14,11 @@ const StatisticsPaper = styled(Paper)(({ theme }) => ({
elevation: 3,
}))
export default function StatisticsBlocks({
- fismaSystems,
scores,
}: {
- fismaSystems: FismaSystemType[]
scores: Record
}) {
+ const { fismaSystems } = useContextProp()
const [totalSystems, setTotalSystems] = useState(0)
const [avgSystemScore, setAvgSystemScore] = useState(0)
const [maxSystemAcronym, setMaxSystemAcronym] = useState('')
@@ -49,11 +48,17 @@ export default function StatisticsBlocks({
}
}
}
+ if (totalCount === 0) {
+ setAvgSystemScore(0)
+ setMinSystemScore(0)
+ } else {
+ setAvgSystemScore(Number((totalScores / totalCount).toFixed(2)))
+ setMinSystemScore(minScore)
+ }
setTotalSystems(totalCount)
- setAvgSystemScore(Number((totalScores / totalCount).toFixed(2)))
setMaxSystemScore(maxScore)
setMaxSystemAcronym(maxScoreSystem || '')
- setMinSystemScore(minScore)
+
setMinSystemAcronym(minScoreSystem || '')
setLoading(false)
}, [fismaSystems, scores])
diff --git a/src/views/Title/Context.ts b/src/views/Title/Context.ts
index 8ca0616..d5b7356 100644
--- a/src/views/Title/Context.ts
+++ b/src/views/Title/Context.ts
@@ -1,8 +1,11 @@
import { useOutletContext } from 'react-router-dom'
-import { FismaSystemType } from '@/types'
+import { FismaSystemType, userData } from '@/types'
-type ContextType = { fismaSystems: FismaSystemType[] | [] }
+type ContextType = {
+ fismaSystems: FismaSystemType[] | []
+ userInfo: userData
+}
-export function useFismaSystems() {
+export function useContextProp() {
return useOutletContext()
}
diff --git a/src/views/Title/Title.tsx b/src/views/Title/Title.tsx
index e26d0c5..7aa8966 100644
--- a/src/views/Title/Title.tsx
+++ b/src/views/Title/Title.tsx
@@ -17,10 +17,15 @@ import { FismaSystemType } from '@/types'
import { Routes } from '@/router/constants'
import axiosInstance from '@/axiosConfig'
import LoginPage from '../LoginPage/LoginPage'
+import { ERROR_MESSAGES } from '@/constants'
+import EditSystemModal from '../EditSystemModal/EditSystemModal'
+import { EMPTY_SYSTEM } from '../EditSystemModal/emptySystem'
+import _ from 'lodash'
/**
* Component that renders the contents of the Dashboard view.
* @returns {JSX.Element} Component that renders the dashboard contents.
*/
+
const emptyUser: userData = {
userid: '',
email: '',
@@ -41,6 +46,7 @@ export default function Title() {
const [anchorEl, setAnchorEl] = useState(null)
const [fismaSystems, setFismaSystems] = useState([])
const [titlePage, setTitlePage] = useState('Dashboard')
+ const [openModal, setOpenModal] = useState(false)
useEffect(() => {
async function fetchFismaSystems() {
try {
@@ -48,7 +54,7 @@ export default function Title() {
if (fismaSystems.status !== 200) {
navigate(Routes.SIGNIN, {
replace: true,
- state: { message: 'Please log in' },
+ state: ERROR_MESSAGES.expired,
})
}
setFismaSystems(fismaSystems.data.data)
@@ -70,6 +76,13 @@ export default function Title() {
const handleClose = () => {
setAnchorEl(null)
}
+ const handleCloseModal = (newRowData: FismaSystemType) => {
+ if (!_.isEqual(EMPTY_SYSTEM, newRowData)) {
+ setFismaSystems((prevFismSystems) => [...prevFismSystems, newRowData])
+ }
+ setOpenModal(false)
+ handleClose()
+ }
return (
<>
@@ -143,6 +156,9 @@ export default function Title() {
Edit Users
+
>
) : (
@@ -164,8 +180,15 @@ export default function Title() {
{loaderData.status !== 200 ? (
) : (
-
+
)}
+
>
)
diff --git a/src/views/UserTable/UserTable.tsx b/src/views/UserTable/UserTable.tsx
index 7a66dad..185dbfa 100644
--- a/src/views/UserTable/UserTable.tsx
+++ b/src/views/UserTable/UserTable.tsx
@@ -32,7 +32,7 @@ import Tooltip from '@mui/material/Tooltip'
import './UserTable.css'
import axiosInstance from '@/axiosConfig'
import { users } from '@/types'
-import { useFismaSystems } from '../Title/Context'
+import { useContextProp } from '../Title/Context'
import Box from '@mui/material/Box'
import CustomSnackbar from '../Snackbar/Snackbar'
import AssignSystemModal from '../AssignSystemModal/AssignSystemModal'
@@ -95,7 +95,7 @@ export default function UserTable() {
}
const [rows, setRows] = useState([])
const [userId, setUserId] = useState('')
- const { fismaSystems } = useFismaSystems()
+ const { fismaSystems } = useContextProp()
const [rowModesModel, setRowModesModel] = useState({})
const [open, setOpen] = useState(false)
const [openModal, setOpenModal] = useState(false)
@@ -135,7 +135,7 @@ export default function UserTable() {
apiRef.current.setEditCellValue({
id: selectedRow.userid,
field: 'role',
- value: selectedRow.role, // Replace with the desired value
+ value: selectedRow.role,
})
}
setOpenAlert(false)