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

[UI] Add Validation and Error Boundaries #8833

Merged
merged 8 commits into from
Sep 20, 2023
867 changes: 451 additions & 416 deletions package-lock.json

Large diffs are not rendered by default.

30 changes: 30 additions & 0 deletions ui/components/General/ErrorBoundary.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ErrorBoundary as ReactErrorBoundary } from "react-error-boundary";

function Fallback({ error }) {
// Call resetErrorBoundary() to reset the error boundary and retry the render.

return (
<div role="alert">
<p>Something went wrong:</p>
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
<p>Something went wrong:</p>
<h3>Please pardon the mesh</h3>

<pre style={{ color : "red" }}>{error.message}</pre>
</div>
)
}

export const ErrorBoundary = ({ children ,...props }) => {
return (
<ReactErrorBoundary FallbackComponent={Fallback} {...props}>
{children}
</ReactErrorBoundary>
);
}

export const withErrorBoundary = (Component) => {
const WrappedWithErrorBoundary = (props) => (
<ErrorBoundary FallbackComponent>
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
<ErrorBoundary FallbackComponent>
<ErrorBoundary Fallback>

Or I am missing something?

<Component {...props} />
</ErrorBoundary>
);

return WrappedWithErrorBoundary;
}
84 changes: 84 additions & 0 deletions ui/components/NotificationCenter/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import AlertIcon from "../../assets/icons/AlertIcon";
import ErrorIcon from "../../assets/icons/ErrorIcon.js"
import { Colors } from "../../themes/app";
import ReadIcon from "../../assets/icons/ReadIcon";
import Ajv from "ajv";
import _ from "lodash";

export const SEVERITY = {
INFO : "informational",
Expand Down Expand Up @@ -37,4 +39,86 @@ export const SEVERITY_STYLE = {
color : NOTIFICATIONCOLORS.WARNING
},

}

//TODO: This should be generated from OPENAPI schema
const EVENT_SCHEMA = {
type : "object",
Copy link
Contributor

Choose a reason for hiding this comment

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

The schema is already present but the contract b/w fronted and backend is missing.

also, this schema is inaccurate as it lacks category and action

Copy link
Contributor Author

Choose a reason for hiding this comment

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

we were not using that in frontend anywhere yet , thats why forgot . will add those

properties : {
id : { type : "string" },
description : {
type : "string",
default : ""
},
severity : {
type : "string",
enum : Object.values(SEVERITY),
default : SEVERITY.INFO
},
status : {
type : "string",
enum : Object.values(STATUS),
default : STATUS.UNREAD
},
created_at : { type : "string" },
updated_at : { type : "string" },
user_id : { type : "string" },
system_id : { type : "string" },
operation_id : { type : "string" },
action : { type : "string" },
metadata : {
type : "object",
}
},
required : ["id", "severity", "status", "created_at", "updated_at", "user_id", "system_id", "action"]
}


// Validate event against EVENT_SCHEMA and return [isValid,validatedEvent]
export const validateEvent = (event) => {
const eventCopy = _.cloneDeep(event) || {};
const ajv = new Ajv({
useDefaults : true,

});
const validate = ajv.compile(EVENT_SCHEMA);
const valid = validate(eventCopy);
return [valid, eventCopy];
}

// return validated events (adds default values if not present)
export const validateEvents = (events) => {
return events.map((event) => {
const [isValid, validatedEvent] = validateEvent(event)
return isValid ? validatedEvent : null
}).filter((event) => event)
}



const EVENT_METADATA_SCHEMA = {
Copy link
Contributor

Choose a reason for hiding this comment

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

This isn’t always the case, in long operations i.e. an operation which invoked multiple other operation the metadata contains a summary which is not an error specifically.

Copy link
Member

Choose a reason for hiding this comment

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

Very good point regarding errors being one of a number of different types of events.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks Uzair , i have not considered that . do you have an example response for this kind of situation

type : "object",
properties : {
error : {
type : "object",
properties : {
Code : { type : "string" },
LongDescription : { type : "array", items : { type : "string" }, default : [] },
ProbableCause : { type : "array", items : { type : "string" }, default : [] },
Severity : { type : "number", default : 1 },
ShortDescription : { type : "array", items : { type : "string" }, default : [] },
SuggestedRemediation : { type : "array", items : { type : "string" }, default : [] },
},
required : ["Code", "LongDescription", "ProbableCause", "Severity", "ShortDescription", "SuggestedRemediation"]
},
},
required : ["error"]
}

export const validateEventMetadata = (metadata) => {
const metadataCopy = _.cloneDeep(metadata) || {};
const ajv = new Ajv();
const validate = ajv.compile(EVENT_METADATA_SCHEMA);
const valid = validate(metadataCopy);
return [valid, metadataCopy];
}
5 changes: 3 additions & 2 deletions ui/components/NotificationCenter/filter.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useGetEventFiltersQuery } from "../../rtk-query/notificationCenter";
import { withErrorBoundary } from "../General/ErrorBoundary";
import TypingFilter from "../TypingFilter";
import { SEVERITY, STATUS } from "./constants";

Expand Down Expand Up @@ -40,9 +41,9 @@ const useFilterSchema = () => {
};
}

const Filter = ({ handleFilter }) => {
const Filter = withErrorBoundary(({ handleFilter }) => {
const filterSchema = useFilterSchema();
return <TypingFilter handleFilter={handleFilter} filterSchema={filterSchema} />;
};
});

export default Filter;
43 changes: 23 additions & 20 deletions ui/components/NotificationCenter/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { closeNotificationCenter, loadEvents, loadNextPage, selectEvents, toggle
import { useGetEventsSummaryQuery, useLazyGetEventsQuery } from "../../rtk-query/notificationCenter";
import _ from "lodash";
import DoneIcon from "../../assets/icons/DoneIcon";
import { ErrorBoundary, withErrorBoundary } from "../General/ErrorBoundary";


const getSeverityCount = (count_by_severity_level, severity) => {
Expand All @@ -30,7 +31,7 @@ const EmptyState = () => {
</Box >)
}

const NavbarNotificationIcon = () => {
const NavbarNotificationIcon = withErrorBoundary(() => {

const { data } = useGetEventsSummaryQuery()
const count_by_severity_level = data?.count_by_severity_level || []
Expand All @@ -53,10 +54,10 @@ const NavbarNotificationIcon = () => {
return (
<BellIcon className={iconMedium} fill="#fff" />
)
}
})


const NotificationCountChip = ({ classes, notificationStyle, count,type, handleClick }) => {
const NotificationCountChip = withErrorBoundary(({ classes, notificationStyle, count,type, handleClick }) => {
const chipStyles = {
fill : notificationStyle?.color,
height : "20px",
Expand All @@ -75,9 +76,9 @@ const NotificationCountChip = ({ classes, notificationStyle, count,type, handleC
</Button>
</Tooltip>
)
}
})

const Header = ({ handleFilter, handleClose }) => {
const Header = withErrorBoundary(({ handleFilter, handleClose }) => {

const { data } = useGetEventsSummaryQuery();
const { count_by_severity_level, total_count } = data || {
Expand All @@ -97,7 +98,7 @@ const Header = ({ handleFilter, handleClose }) => {
})
}

const archivedCount = total_count - count_by_severity_level
const unreadCount = total_count - count_by_severity_level
.reduce((acc, item) => acc + item.count, 0)
return (
<div className={classNames(classes.container, classes.header)}>
Expand All @@ -118,12 +119,12 @@ const Header = ({ handleFilter, handleClose }) => {
notificationStyle={STATUS_STYLE[STATUS.READ]}
handleClick={() => onClickStatus(STATUS.READ)}
type={STATUS.READ}
count={archivedCount} />
count={unreadCount} />

</div>
</div>
)
}
})

const Loading = () => {
return (
Expand All @@ -133,7 +134,7 @@ const Loading = () => {
}


const EventsView = ({ handleLoadNextPage, isFetching, hasMore }) => {
const EventsView = withErrorBoundary(({ handleLoadNextPage, isFetching, hasMore }) => {
const events = useSelector(selectEvents)
// const page = useSelector((state) => state.events.current_view.page);

Expand Down Expand Up @@ -173,12 +174,11 @@ const EventsView = ({ handleLoadNextPage, isFetching, hasMore }) => {
{isFetching && hasMore && <Loading />}
</>
)
}
})

const CurrentFilterView = ({ handleFilter }) => {
const CurrentFilterView = withErrorBoundary( ({ handleFilter }) => {

const currentFilters = useSelector((state) => state.events.current_view.filters);

const onDelete = (key, value) => {
const newFilters = {
...currentFilters,
Expand Down Expand Up @@ -216,7 +216,7 @@ const CurrentFilterView = ({ handleFilter }) => {

</div>
)
}
})


const MesheryNotification = () => {
Expand Down Expand Up @@ -257,7 +257,7 @@ const MesheryNotification = () => {
}

return (
<NoSsr>
<>
<div>
<IconButton
id="notification-button"
Expand Down Expand Up @@ -312,7 +312,7 @@ const MesheryNotification = () => {
</div>
</Drawer>
</ClickAwayListener>
</NoSsr>
</>
);
};

Expand All @@ -334,11 +334,14 @@ const MesheryNotification = () => {
const NotificationCenter = (props) => {

return (
<>
<Provider store={store} >
<MesheryNotification {...props} />
</Provider >
</>

<NoSsr>
<ErrorBoundary FallbackComponent={() => null} onError={e => console.error("Error in NotificationCenter",e)}>
<Provider store={store} >
<MesheryNotification {...props} />
</Provider >
</ErrorBoundary>
</NoSsr>
)

};
Expand Down