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

fix(catalog): Withdrawn "top" controls display correctly #720

Merged

Conversation

Bronstrom
Copy link
Contributor

@Bronstrom Bronstrom commented Mar 7, 2023

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

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.
Copy link
Contributor

@tuckerzp tuckerzp left a comment

Choose a reason for hiding this comment

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

This looks like a pretty good start so far! I had a small comment and a thought.

I will also post this for other's reviewing
image

const withdrawn = isWithdrawn(control);
const itemText = `${control.id.toUpperCase()} ${control.title}`;
// Make collapsible item when not a withdrawn control
return !withdrawn ? (
Copy link
Contributor

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.

src/components/OSCALCatalogGroup.js Outdated Show resolved Hide resolved
Comment on lines 11 to 15
export default function isWithdrawn(control) {
return control?.props?.find(
(prop) => prop.name === "status" && prop.value === "withdrawn"
);
}
Copy link
Contributor

@kylelaker kylelaker Mar 7, 2023

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.

Copy link
Contributor

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)

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

@kylelaker
Copy link
Contributor

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?)

@Bronstrom
Copy link
Contributor Author

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 theme.palette color wasn't declared there and just a hex code for a gray color. I ended up just matching the top-level controls to this. It would probably be best we have these align to the theme.palette. In the past we have just declared something like theme.palette.grey[50] to produce darker/lighter shades of palette colors, which could work in this case. I believe "50" aligns to opacity.

@Bronstrom
Copy link
Contributor Author

In the past we have just declared something like theme.palette.grey[50] to produce darker/lighter shades of palette colors, which could work in this case. I believe "50" aligns to opacity.

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.

image

@@ -24,6 +25,11 @@ const StyledListItemPaper = styled(Paper)`
border-radius: 0.5em;
`;

const StyledListItemText = styled(ListItemText)(({ theme }) => ({
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
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.

Comment on lines 6 to 8
* 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
Copy link
Contributor

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.

Copy link
Contributor Author

@Bronstrom Bronstrom Mar 9, 2023

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?

Comment on lines 7 to 12
/**
* Return the given namespace, or if undefined, the default namespace.
*/
export function namespaceOf(ns) {
return ns ?? NIST_DEFAULT_NAMESPACE;
}
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
/**
* 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;
}

Comment on lines 14 to 20
/**
* 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) {
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
/**
* 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.

Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Contributor Author

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.

@Bronstrom Bronstrom marked this pull request as ready for review March 9, 2023 19:10
Co-Authored-By: Kyle Laker <klaker@easydynamics.com>
Copy link
Contributor

@kylelaker kylelaker left a comment

Choose a reason for hiding this comment

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

Good stuff!

@easy-dynamics-oscal-automation easy-dynamics-oscal-automation bot requested a review from a team March 9, 2023 19:32
* Determines if a control is withdrawn.
*
* @param {object} control
* @returns a boolean describing whether the control is withdrawn
Copy link
Contributor

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?

Suggested change
* @returns a boolean describing whether the control is withdrawn
* @returns {boolean} true if control is withdrawn

@easy-dynamics-oscal-automation easy-dynamics-oscal-automation bot requested a review from a team March 9, 2023 19:49
Copy link
Contributor

@tuckerzp tuckerzp left a 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

@kylelaker kylelaker merged commit 3e40c31 into develop Mar 9, 2023
@kylelaker kylelaker deleted the brad/withdrawn-top-level-controls-not-identifiable branch March 9, 2023 21:09
@kylelaker kylelaker added the bug Something isn't working label Jun 12, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Withdrawn "top-level" controls are no longer identifiable
3 participants