-
Notifications
You must be signed in to change notification settings - Fork 11
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
fix(catalog): Withdrawn "top" controls display correctly #720
fix(catalog): Withdrawn "top" controls display correctly #720
Conversation
This fixes the "state" of withdrawn "top-level" controls. Instead of being a collapsible item that shows a blank box when selected, the control no longer opens and closes, as indicated by no arrow appearing to the far right of the item. The item text is also styled to appear a light-gray and has a strikethrough, like the "sub-level" controls.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const withdrawn = isWithdrawn(control); | ||
const itemText = `${control.id.toUpperCase()} ${control.title}`; | ||
// Make collapsible item when not a withdrawn control | ||
return !withdrawn ? ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is kind of just a general thought, but I wish that we did not use these super long ternary operators everywhere. They are very gross. Not sure if there is much you can do to avoid it here however.
export default function isWithdrawn(control) { | ||
return control?.props?.find( | ||
(prop) => prop.name === "status" && prop.value === "withdrawn" | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's actually do namespace validation. Some of this code probably belongs in a different file but I am thinking of an API a bit like this (ignore the types if the resulting file has to be JS):
/**
* When a namespace is not specified for a property, the assumption is that the
* namespace is default namespace.
*/
const NIST_DEFAULT_NAMESPACE = "https://csrc.nist.gov/ns/oscal";
export interface PropFilter {
/**
* The namespace of the property.
*
* @default https://csrc.nist.gov/ns/oscal
*/
ns?: string;
/**
* The name of the prop to find.
*/
name: string;
/**
* The acceptable value of the property.
*
* Either this property or {@link filter} (but not both) must be specified.
*/
value?: string;
/**
* A function to validate the property value.
*
* Either this property or {@link value} (but not both) must be specified.
*/
filter?: (value: string) => boolean;
}
/**
* Return the given namespace, or if undefined, the default namespace.
*/
export function namespaceOf(ns: string | undefined): string {
return ns ?? NIST_DEFAULT_NAMESPACE;
}
/**
* Returns the first matching property in the given list.
*
* @param props the list of properties to check
* @param filter the attributes to match against
*/
export function matchingProp(props: Prop[], filter: PropFilter): Prop | undefined {
const ns = namespaceof(filter.ns);
if ((!filter.value && !filter.filter) || (filter.value && filter.filter)) {
throw new Error("Exactly one of filter or value must be specified");
}
return props.find((prop) =>
namespaceOf(prop.ns) === namespaceOf(filter.namespace)
&& prop.name === filter.name
&& (!filter.value || prop.value === filter.value)
&& (!filter.filter || filter.filter(value))
);
}
export function isWithdrawn(control: Control): boolean {
// By default controls are *not* withdrawn
if (!control.props) {
return false;
}
return !!matchingProp(control.props, { name: "status", value: "withdrawn });
}
Besides isWithdrawn
, most of this probably deserves to exist more in an oscal-utils/OSCALPropUtils.js
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Eventually, we may want to support class
in that filter interface but I do not want to deal with that today)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've gone ahead and added namespace validation for JS - types have been removed as well as the interface for now. There were a couple other variable corrections I had to make, but I was able to figure them out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've separated isWithdrawn
from the rest of the functions oscal-utils/OSCALPropUtils.js
but followed the same logic as mentioned. I've just kept the isWithdrawn
function in the Catalog Utils class I created. Let me know if something else works better here.
I am also worried that the font color with the background could be an accessibility issue... I wonder if there's something we can do there (or maybe we wait and try to work on a high-contrast theme later?) |
Where the other withdrawn controls appear (sub-level controls), a |
Co-Authored-By: Kyle Laker <klaker@easydynamics.com>
This actually refers to shade in material UI, which has a range of 50 (lightest) to 900 (darkest). We consistently use shade 50 and 400. 400 seems to strike a better balance for now, not too light for the background and not too dark to separate withdrawn controls not withdrawn controls. |
src/components/OSCALCatalogGroup.js
Outdated
@@ -24,6 +25,11 @@ const StyledListItemPaper = styled(Paper)` | |||
border-radius: 0.5em; | |||
`; | |||
|
|||
const StyledListItemText = styled(ListItemText)(({ theme }) => ({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const StyledListItemText = styled(ListItemText)(({ theme }) => ({ | |
const WithdrawnListItemText = styled(ListItemText)(({ theme }) => ({ |
Let's specify how it's styled and what it actually represents. Imagine some day we want to signify "important" controls (or some other made up descriptor) somehow -- those would be styled too but perhaps in a different way. So StyledListItemText
doesn't really get the point across in the same way.
I know we've made this mistake elsewhere, but let's do better here.
* TODO: This is probably 800-53 specific and it should be made more | ||
* generic to allow the library to work with other frameworks. | ||
* https://github.com/EasyDynamics/oscal-react-library/issues/504 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is now handled. props[@name="status" and @value="withdrawn"]
is how to do this per the spec and we now handle the namespace field correctly for this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh true. I guess we could close out #504 once this PR is merged?
/** | ||
* Return the given namespace, or if undefined, the default namespace. | ||
*/ | ||
export function namespaceOf(ns) { | ||
return ns ?? NIST_DEFAULT_NAMESPACE; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/** | |
* Return the given namespace, or if undefined, the default namespace. | |
*/ | |
export function namespaceOf(ns) { | |
return ns ?? NIST_DEFAULT_NAMESPACE; | |
} | |
/** | |
* Return the given namespace, or if undefined, the default namespace. | |
* | |
* @param {string | undefined} ns the namespace | |
* @returns {string} the given namespace or the the default namespace | |
*/ | |
export function namespaceOf(ns) { | |
return ns ?? NIST_DEFAULT_NAMESPACE; | |
} |
/** | ||
* Returns the first matching property in the given list. | ||
* | ||
* @param props the list of properties to check | ||
* @param filter the attributes to match against | ||
*/ | ||
export default function matchingProp(props, filter) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/** | |
* Returns the first matching property in the given list. | |
* | |
* @param props the list of properties to check | |
* @param filter the attributes to match against | |
*/ | |
export default function matchingProp(props, filter) { | |
/** | |
* @callback PropertyFilterValueMatcher | |
* @param {string} value - the value to check against | |
* @returns {boolean} whether the given property matches | |
*/ | |
/** | |
* A filter to apply to properties to find a matching property. | |
* | |
* This allows matching based on the namespace (defaulting to the default namespace), | |
* the name, and the value. Exactly one of `value` or `filter` must be supplied. | |
* | |
* @typedef PropertyFilter | |
* @type {object} | |
* @property {string|undefined} ns - the namespace to check in | |
* @property {string| name - the name of the field to match | |
* @property {string} value - the value of the field to match | |
* @property {PropertyFilterValueMatcher} filter - a function to match the value against | |
*/ | |
/** | |
* Returns the first matching property in the given list. | |
* | |
* @param props {Array.<Object>} the list of properties to check | |
* @param filter {PropertyFilter} the attributes to match against | |
*/ | |
export default function matchingProp(props, filter) { |
I think we can represent the types from before as JSDoc types if we want to... I don't know whether this is useful enough or not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would this info make more sense to add once we add TypeScript? . . . or maybe this could be helpful to who implements TS here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair yeah -- maybe we just add it then.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewing over it again, this documentation is valuable even now with JS and TS support is just around the corner.
Co-Authored-By: Kyle Laker <klaker@easydynamics.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good stuff!
* Determines if a control is withdrawn. | ||
* | ||
* @param {object} control | ||
* @returns a boolean describing whether the control is withdrawn |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we also update this @returns
?
* @returns a boolean describing whether the control is withdrawn | |
* @returns {boolean} true if control is withdrawn |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks great! Thanks for your work on this @Bronstrom
This fixes the "state" of withdrawn "top-level"
controls. Instead of being a collapsible item
that shows a blank box when selected, the
control no longer opens and closes, as
indicated by no presence of an arrow
appearing to the far right of the item. The
item text is also styled to appear a light-gray
and has a strikethrough, like the "sub-level"
controls.
Resolves #635