From 648ae730c777c6d2ba74535b7189ae8def6ad8cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20H=2E=20K=C3=B6hler?= Date: Thu, 29 Aug 2024 10:49:43 -0300 Subject: [PATCH] feat - 194: ROI class based color settings (#227) * feat: add class based color settings * fix: update yarn.lock * fix: require oidc and change server * chore: make preview url fixed * fix: failing tests * feat: change annotation categories to use SR instead of ANN * feat: enable changing colors for ROIs * Use live channel * fix: lint * fix: update docker-compose command * fix: use black text * feat: automatically assign colors to categories * fix: annotation category style is undefined * chore: extract color constants * fix: visibility state when style change --------- Co-authored-by: Igor Octaviano --- .github/workflows/container-tests.yml | 2 +- .github/workflows/deploy-to-firebase.yml | 1 + public/config/preview.js | 12 +- src/components/AnnotationCategoryItem.tsx | 97 ++++++-- src/components/AnnotationCategoryList.tsx | 45 ++-- src/components/AnnotationGroupItem.tsx | 22 +- src/components/AnnotationGroupList.tsx | 4 +- src/components/ColorSettingsMenu.tsx | 267 ++++++++++++++++++++++ src/components/SlideViewer.tsx | 123 ++++++++-- src/services/NotificationMiddleware.js | 4 +- src/services/RoiToAnnotationAdapter.ts | 44 ++++ types/dicom-microscopy-viewer/index.d.ts | 2 +- yarn.lock | 20 +- 13 files changed, 560 insertions(+), 83 deletions(-) create mode 100644 src/components/ColorSettingsMenu.tsx create mode 100644 src/services/RoiToAnnotationAdapter.ts diff --git a/.github/workflows/container-tests.yml b/.github/workflows/container-tests.yml index 5f5d422..9063957 100644 --- a/.github/workflows/container-tests.yml +++ b/.github/workflows/container-tests.yml @@ -20,7 +20,7 @@ jobs: node-version: 20.8.1 - name: Build and run containers using docker compose - run: docker-compose up -d + run: docker compose up -d - name: Test web page run: | diff --git a/.github/workflows/deploy-to-firebase.yml b/.github/workflows/deploy-to-firebase.yml index 35c6d70..a2e23a9 100644 --- a/.github/workflows/deploy-to-firebase.yml +++ b/.github/workflows/deploy-to-firebase.yml @@ -35,3 +35,4 @@ jobs: repoToken: "${{ secrets.GITHUB_TOKEN }}" firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_SLIM }}" projectId: idc-external-006 + channelId: live diff --git a/public/config/preview.js b/public/config/preview.js index 9436e24..2c973fa 100644 --- a/public/config/preview.js +++ b/public/config/preview.js @@ -3,14 +3,20 @@ window.config = { servers: [ { id: 'preview', - url: 'https://idc-external-006.uc.r.appspot.com/dcm4chee-arc/aets/DCM4CHEE/rs', + url: 'https://proxy.imaging.datacommons.cancer.gov/current/viewer-only-no-downloads-see-tinyurl-dot-com-slash-3j3d9jyp/dicomWeb', write: false } ], + oidc: { + authority: 'https://accounts.google.com', + clientId: '293449031882-k4um45hl4g94fsgbnviel0lh38836i9v.apps.googleusercontent.com', + scope: 'email profile openid https://www.googleapis.com/auth/cloud-healthcare', + grantType: 'implicit' + }, disableWorklist: false, disableAnnotationTools: false, enableServerSelection: true, - mode: "light", + mode: 'light', preload: true, annotations: [ { @@ -24,6 +30,6 @@ window.config = { color: [255, 255, 255, 0.2] } } - }, + } ] } diff --git a/src/components/AnnotationCategoryItem.tsx b/src/components/AnnotationCategoryItem.tsx index acf1cd3..a5ef931 100644 --- a/src/components/AnnotationCategoryItem.tsx +++ b/src/components/AnnotationCategoryItem.tsx @@ -1,15 +1,26 @@ import React from 'react' -import { Menu, Space, Checkbox, Tooltip } from 'antd' +import { Menu, Space, Checkbox, Tooltip, Popover, Button } from 'antd' +import { SettingOutlined } from '@ant-design/icons' import { Category, Type } from './AnnotationCategoryList' +import ColorSettingsMenu from './ColorSettingsMenu' -const AnnotationGroupItem = ({ +const AnnotationCategoryItem = ({ category, onChange, - checkedAnnotationGroupUids + checkedAnnotationUids, + onStyleChange, + defaultAnnotationStyles }: { category: Category onChange: Function - checkedAnnotationGroupUids: Set + onStyleChange: Function + defaultAnnotationStyles: { + [annotationUID: string]: { + opacity: number + color: number[] + } + } + checkedAnnotationUids: Set }): JSX.Element => { const { types } = category @@ -21,12 +32,12 @@ const AnnotationGroupItem = ({ } const checkAll = types.every((type: Type) => - type.uids.every((uid: string) => checkedAnnotationGroupUids.has(uid)) + type.uids.every((uid: string) => checkedAnnotationUids.has(uid)) ) const indeterminate = !checkAll && types.some((type: Type) => - type.uids.some((uid: string) => checkedAnnotationGroupUids.has(uid)) + type.uids.some((uid: string) => checkedAnnotationUids.has(uid)) ) const handleChangeCheckedType = ({ @@ -37,7 +48,7 @@ const AnnotationGroupItem = ({ isVisible: boolean }): void => { type.uids.forEach((uid: string) => { - onChange({ annotationGroupUID: uid, isVisible }) + onChange({ roiUID: uid, isVisible }) }) } @@ -47,7 +58,7 @@ const AnnotationGroupItem = ({ key={category.CodeMeaning} > -
+
{category.CodeMeaning} + ( + { + return [...acc, ...type.uids] + }, + [] + )} + onStyleChange={onStyleChange} + defaultStyle={ + defaultAnnotationStyles[types[0].uids[0]] + } + /> + )} + > +
) })} @@ -98,4 +165,4 @@ const AnnotationGroupItem = ({ ) } -export default AnnotationGroupItem +export default AnnotationCategoryItem diff --git a/src/components/AnnotationCategoryList.tsx b/src/components/AnnotationCategoryList.tsx index 4411d99..f062f6f 100644 --- a/src/components/AnnotationCategoryList.tsx +++ b/src/components/AnnotationCategoryList.tsx @@ -1,8 +1,12 @@ import React from 'react' import { Menu } from 'antd' -import * as dmv from 'dicom-microscopy-viewer' import AnnotationCategoryItem from './AnnotationCategoryItem' +export interface AnnotationCategoryAndType { + uid: string + type: Omit + category: Omit +} export interface Type { CodeValue: string CodeMeaning: string @@ -16,22 +20,22 @@ export interface Category { types: Type[] } -const getCategories = (annotationGroups: any): Record => { - const categories = annotationGroups?.reduce( +const getCategories = (annotations: any): Record => { + const categories = annotations?.reduce( ( categoriesAcc: Record }>, - annotationGroup: dmv.annotation.AnnotationGroup + annotation: AnnotationCategoryAndType ) => { - const { propertyCategory, propertyType, uid } = annotationGroup - const categoryKey = propertyCategory.CodeMeaning - const typeKey = propertyType.CodeMeaning + const { category, type, uid } = annotation + const categoryKey = category.CodeMeaning + const typeKey = type.CodeMeaning const oldCategory = categoriesAcc[categoryKey] ?? { - ...propertyCategory, + ...category, types: {} } const oldType = oldCategory.types[typeKey] ?? { - ...propertyType, + ...type, uids: [] } @@ -63,15 +67,24 @@ const getCategories = (annotationGroups: any): Record => { } const AnnotationCategoryList = ({ - annotationGroups, + annotations, onChange, - checkedAnnotationGroupUids + onStyleChange, + defaultAnnotationStyles, + checkedAnnotationUids }: { - annotationGroups: dmv.annotation.AnnotationGroup[] + annotations: AnnotationCategoryAndType[] onChange: Function - checkedAnnotationGroupUids: Set + onStyleChange: Function + defaultAnnotationStyles: { + [annotationUID: string]: { + opacity: number + color: number[] + } + } + checkedAnnotationUids: Set }): JSX.Element => { - const categories: Record = getCategories(annotationGroups) + const categories: Record = getCategories(annotations) if (Object.keys(categories).length === 0) { return <> @@ -84,7 +97,9 @@ const AnnotationCategoryList = ({ key={category.CodeMeaning} category={category} onChange={onChange} - checkedAnnotationGroupUids={checkedAnnotationGroupUids} + onStyleChange={onStyleChange} + defaultAnnotationStyles={defaultAnnotationStyles} + checkedAnnotationUids={checkedAnnotationUids} /> ) }) diff --git a/src/components/AnnotationGroupItem.tsx b/src/components/AnnotationGroupItem.tsx index c990a88..b795a30 100644 --- a/src/components/AnnotationGroupItem.tsx +++ b/src/components/AnnotationGroupItem.tsx @@ -32,8 +32,8 @@ interface AnnotationGroupItemProps { annotationGroupUID: string isVisible: boolean }) => void - onStyleChange: ({ annotationGroupUID, styleOptions }: { - annotationGroupUID: string + onStyleChange: ({ uid, styleOptions }: { + uid: string styleOptions: { opacity?: number color?: number[] @@ -89,7 +89,7 @@ class AnnotationGroupItem extends React.Component ({ @@ -294,7 +294,7 @@ class AnnotationGroupItem extends React.Component void - onAnnotationGroupStyleChange: ({ annotationGroupUID, styleOptions }: { - annotationGroupUID: string + onAnnotationGroupStyleChange: ({ uid, styleOptions }: { + uid: string styleOptions: { opacity?: number color?: number[] diff --git a/src/components/ColorSettingsMenu.tsx b/src/components/ColorSettingsMenu.tsx new file mode 100644 index 0000000..09e986c --- /dev/null +++ b/src/components/ColorSettingsMenu.tsx @@ -0,0 +1,267 @@ +import React from 'react' +import { Col, Divider, InputNumber, Row, Slider } from 'antd' + +interface ColorSettingsMenuProps { + annotationGroupsUIDs: string[] + defaultStyle: { + opacity: number + color: number[] + } + onStyleChange: Function +} + +interface ColorSettingsMenuState { + currentStyle: { + opacity: number + color?: number[] + } +} + +/** + * React component representing an Annotation Group. + */ +class ColorSettingsMenu extends React.Component< +ColorSettingsMenuProps, +ColorSettingsMenuState +> { + constructor (props: ColorSettingsMenuProps) { + super(props) + this.handleOpacityChange = this.handleOpacityChange.bind(this) + this.handleColorRChange = this.handleColorRChange.bind(this) + this.handleColorGChange = this.handleColorGChange.bind(this) + this.handleColorBChange = this.handleColorBChange.bind(this) + this.getCurrentColor = this.getCurrentColor.bind(this) + this.state = { + currentStyle: { + opacity: this.props.defaultStyle.opacity, + color: this.props.defaultStyle.color + } + } + } + + handleOpacityChange (value: number | null): void { + if (value != null) { + this.props.annotationGroupsUIDs.forEach((uid) => { + this.props.onStyleChange({ + uid, + styleOptions: { + color: this.state.currentStyle.color, + opacity: value + } + }) + }) + this.setState({ + currentStyle: { + opacity: value, + color: this.state.currentStyle.color + } + }) + } + } + + handleColorRChange (value: number | number[] | null): void { + if (value != null && this.state.currentStyle.color !== undefined) { + const color = [ + Array.isArray(value) ? value[0] : value, + this.state.currentStyle.color[1], + this.state.currentStyle.color[2] + ] + this.setState((state) => ({ + currentStyle: { + color: color, + opacity: state.currentStyle.opacity + } + })) + this.props.annotationGroupsUIDs.forEach((uid) => { + this.props.onStyleChange({ + uid, + styleOptions: { + color: color, + opacity: this.state.currentStyle.opacity + } + }) + }) + } + } + + handleColorGChange (value: number | number[] | null): void { + if (value != null && this.state.currentStyle.color !== undefined) { + const color = [ + this.state.currentStyle.color[0], + Array.isArray(value) ? value[0] : value, + this.state.currentStyle.color[2] + ] + this.setState((state) => ({ + currentStyle: { + color: color, + opacity: state.currentStyle.opacity + } + })) + this.props.annotationGroupsUIDs.forEach((uid) => { + this.props.onStyleChange({ + uid, + styleOptions: { + color: color, + opacity: this.state.currentStyle.opacity + } + }) + }) + } + } + + handleColorBChange (value: number | number[] | null): void { + if (value != null && this.state.currentStyle.color !== undefined) { + const color = [ + this.state.currentStyle.color[0], + this.state.currentStyle.color[1], + Array.isArray(value) ? value[0] : value + ] + this.setState((state) => ({ + currentStyle: { + color: color, + opacity: state.currentStyle.opacity + } + })) + + this.props.annotationGroupsUIDs.forEach((uid) => { + this.props.onStyleChange({ + uid, + styleOptions: { + color: color, + opacity: this.state.currentStyle.opacity + } + }) + }) + } + } + + getCurrentColor (): string { + const rgb2hex = (values: number[]): string => { + const r = values[0] + const g = values[1] + const b = values[2] + return '#' + (0x1000000 + (r << 16) + (g << 8) + b).toString(16).slice(1) + } + + if (this.state.currentStyle.color != null) { + return rgb2hex(this.state.currentStyle.color) + } else { + return 'white' + } + } + + render (): React.ReactNode { + let colorSettings + if (this.state.currentStyle.color != null) { + colorSettings = ( + <> + Color + + Red + + + + + + + + + + Green + + + + + + + + + + Blue + + + + + + + + + + ) + } + + return ( +
+ {colorSettings} + + Opacity + + + + + + + +
+ ) + } +} + +export default ColorSettingsMenu diff --git a/src/components/SlideViewer.tsx b/src/components/SlideViewer.tsx index 4ed9fed..b434c94 100644 --- a/src/components/SlideViewer.tsx +++ b/src/components/SlideViewer.tsx @@ -50,12 +50,25 @@ import NotificationMiddleware, { } from '../services/NotificationMiddleware' import AnnotationCategoryList from './AnnotationCategoryList' import HoveredRoiTooltip from './HoveredRoiTooltip' +import { adaptRoiToAnnotation } from '../services/RoiToAnnotationAdapter' const DEFAULT_ROI_STROKE_COLOR: number[] = [255, 234, 0] // [0, 126, 163] const DEFAULT_ROI_FILL_COLOR: number[] = [255, 234, 0, 0.2] // [0, 126, 163, 0.2] const DEFAULT_ROI_STROKE_WIDTH: number = 2 const DEFAULT_ROI_RADIUS: number = 5 +const DEFAULT_ANNOTATION_OPACITY = 0.4 +const DEFAULT_ANNOTATION_STROKE_COLOR = [0, 0, 0] +const DEFAULT_ANNOTATION_COLOR_PALETTE = [ + [54, 162, 235], + [181, 65, 98], + [75, 192, 192], + [255, 158, 64], + [153, 102, 254], + [255, 205, 86], + [200, 203, 207] +] + const _buildKey = (concept: { CodeValue: string CodeMeaning: string @@ -454,6 +467,13 @@ class SlideViewer extends React.Component { private roiStyles: {[key: string]: dmv.viewer.ROIStyleOptions} = {} + private defaultAnnotationStyles: { + [annotationUID: string]: { + opacity: number + color: number[] + } + } = {} + private readonly selectionColor: number[] = [140, 184, 198] private readonly selectedRoiStyle: dmv.viewer.ROIStyleOptions = { @@ -539,6 +559,7 @@ class SlideViewer extends React.Component { this.handleAnnotationVisibilityChange = this.handleAnnotationVisibilityChange.bind(this) this.handleAnnotationGroupVisibilityChange = this.handleAnnotationGroupVisibilityChange.bind(this) this.handleAnnotationGroupStyleChange = this.handleAnnotationGroupStyleChange.bind(this) + this.handleRoiStyleChange = this.handleRoiStyleChange.bind(this) this.handleGoTo = this.handleGoTo.bind(this) this.handleXCoordinateSelection = this.handleXCoordinateSelection.bind(this) this.handleYCoordinateSelection = this.handleYCoordinateSelection.bind(this) @@ -2403,7 +2424,8 @@ class SlideViewer extends React.Component { console.info(`show ROI ${roiUID}`) const roi = this.volumeViewer.getROI(roiUID) const key = _getRoiKey(roi) - this.volumeViewer.setROIStyle(roi.uid, this.getRoiStyle(key)) + const style = this.getRoiStyle(key) + this.volumeViewer.setROIStyle(roi.uid, style) this.setState(state => { const visibleRoiUIDs = state.visibleRoiUIDs visibleRoiUIDs.add(roi.uid) @@ -2469,18 +2491,18 @@ class SlideViewer extends React.Component { /** * Handle change of annotation group style. */ - handleAnnotationGroupStyleChange ({ annotationGroupUID, styleOptions }: { - annotationGroupUID: string + handleAnnotationGroupStyleChange ({ uid, styleOptions }: { + uid: string styleOptions: { opacity?: number color?: number[] measurement?: dcmjs.sr.coding.CodedConcept } }): void { - console.log(`change style of annotation group ${annotationGroupUID}`) + console.log(`change style of annotation group ${uid}`) try { this.volumeViewer.setAnnotationGroupStyle( - annotationGroupUID, + uid, styleOptions ) } catch (error) { @@ -2496,6 +2518,52 @@ class SlideViewer extends React.Component { } } + generateRoiStyle ( + styleOptions: { + opacity?: number + color?: number[] + }): dmv.viewer.ROIStyleOptions { + const opacity = styleOptions.opacity ?? DEFAULT_ANNOTATION_OPACITY + const strokeColor = styleOptions.color ?? DEFAULT_ANNOTATION_STROKE_COLOR + const fillColor = strokeColor.map((c) => Math.min(c + 25, 255)) + const style = _formatRoiStyle({ + fill: { color: [...fillColor, opacity] }, + stroke: { color: [...strokeColor, opacity] }, + radius: this.defaultRoiStyle.stroke?.width + }) + return style + } + + handleRoiStyleChange ({ uid, styleOptions }: { + uid: string + styleOptions: { + opacity: number + color: number[] + } + }): void { + console.log(`change style of ROI ${uid}`) + try { + this.defaultAnnotationStyles[uid] = styleOptions + const style = this.generateRoiStyle(styleOptions) + + const roi = this.volumeViewer.getROI(uid) + const key = _getRoiKey(roi) as string + this.roiStyles[key] = style + this.volumeViewer.setROIStyle(uid, style) + this.state.visibleRoiUIDs.add(uid) + } catch (error) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + NotificationMiddleware.onError( + NotificationMiddlewareContext.SLIM, + new CustomError( + errorTypes.VISUALIZATION, + 'Failed to change style of ROI.' + ) + ) + throw error + } + } + /** * Handle toggling of segment visibility, i.e., whether a given * segment should be either displayed or hidden by the viewer. @@ -2981,6 +3049,8 @@ class SlideViewer extends React.Component { ) annotationGroups.push(...filteredAnnotationGroups) + const annotations = rois.map(roi => adaptRoiToAnnotation(roi)) + const openSubMenuItems = [ 'specimens', 'optical-paths', 'annotations', 'presentation-states' ] @@ -3277,16 +3347,37 @@ class SlideViewer extends React.Component { } let annotationGroupMenu + + if (annotations.length > 0) { + annotations.forEach((annotation) => { + const roi = this.volumeViewer.getROI(annotation.uid) + const key = _getRoiKey(roi) as string + const color = this.roiStyles[key] !== undefined + ? this.roiStyles[key].stroke?.color.slice(0, 3) + : DEFAULT_ANNOTATION_COLOR_PALETTE[ + Object.keys(this.roiStyles).length % DEFAULT_ANNOTATION_COLOR_PALETTE.length + ] + this.defaultAnnotationStyles[annotation.uid] = { + color, + opacity: DEFAULT_ANNOTATION_OPACITY + } as any + + this.roiStyles[key] = this.generateRoiStyle( + this.defaultAnnotationStyles[annotation.uid] + ) + }) + } + if (annotationGroups.length > 0) { + const annotationGroupMetadata: { + [annotationGroupUID: string]: dmv.metadata.MicroscopyBulkSimpleAnnotations + } = {} const defaultAnnotationGroupStyles: { - [annotationGroupUID: string]: { + [annotationUID: string]: { opacity: number color: number[] } } = {} - const annotationGroupMetadata: { - [annotationGroupUID: string]: dmv.metadata.MicroscopyBulkSimpleAnnotations - } = {} annotationGroups.forEach(annotationGroup => { defaultAnnotationGroupStyles[annotationGroup.uid] = this.volumeViewer.getAnnotationGroupStyle( annotationGroup.uid @@ -3300,6 +3391,8 @@ class SlideViewer extends React.Component { { {annotationMenuItems} {annotationGroupMenu} - {annotationGroups.length === 0 + {annotations.length === 0 ? ( <> ) @@ -3692,11 +3785,11 @@ class SlideViewer extends React.Component { title='Annotation Categories' > )} diff --git a/src/services/NotificationMiddleware.js b/src/services/NotificationMiddleware.js index 8e1b4ba..72e6b79 100644 --- a/src/services/NotificationMiddleware.js +++ b/src/services/NotificationMiddleware.js @@ -54,7 +54,7 @@ const NotificationSourceDefinition = { } class NotificationMiddleware extends PubSub { - constructor() { + constructor () { super() const outerContext = (args) => { @@ -62,7 +62,7 @@ class NotificationMiddleware extends PubSub { } (function () { - var warn = console.warn; + const warn = console.warn console.warn = function () { if (!JSON.stringify(arguments).includes('request')) { outerContext(arguments) diff --git a/src/services/RoiToAnnotationAdapter.ts b/src/services/RoiToAnnotationAdapter.ts new file mode 100644 index 0000000..5a4bbf7 --- /dev/null +++ b/src/services/RoiToAnnotationAdapter.ts @@ -0,0 +1,44 @@ +import * as dmv from 'dicom-microscopy-viewer' +import * as dcmjs from 'dcmjs' +import { AnnotationCategoryAndType } from '../components/AnnotationCategoryList' + +export const adaptRoiToAnnotation = (roi: dmv.roi.ROI): AnnotationCategoryAndType => { + const { uid, evaluations } = roi + + const result = { + category: { + CodeValue: 'undefined', + CodeMeaning: 'undefined', + CodingSchemeDesignator: 'undefined' + }, + type: { + CodeValue: 'undefined', + CodeMeaning: 'undefined', + CodingSchemeDesignator: 'undefined' + } + } + + evaluations.forEach(( + item: ( + dcmjs.sr.valueTypes.TextContentItem | + dcmjs.sr.valueTypes.CodeContentItem + ) + ) => { + const nameValue = item.ConceptNameCodeSequence[0].CodeValue + if (item.ValueType === dcmjs.sr.valueTypes.ValueTypes.CODE) { + const codeContentItem = item as dcmjs.sr.valueTypes.CodeContentItem + const value = codeContentItem.ConceptCodeSequence[0] + // For consistency with Segment and Annotation Group + if (nameValue === '276214006') { + result.category = { ...value } + } else if (nameValue === '121071') { + result.type = { ...value } + } + } + }) + + return { + ...result, + uid + } +} diff --git a/types/dicom-microscopy-viewer/index.d.ts b/types/dicom-microscopy-viewer/index.d.ts index 4f57751..98496aa 100644 --- a/types/dicom-microscopy-viewer/index.d.ts +++ b/types/dicom-microscopy-viewer/index.d.ts @@ -576,7 +576,7 @@ declare module 'dicom-microscopy-viewer' { SpecimenDescriptionSequence: SpecimenDescription[] OpticalPathSequence: OpticalPath[] AnnotationGroupSequence: Array<{ - SOPClassUID: string, + SOPClassUID: string AnnotationGroupNumber: number AnnotationGroupUID: string AnnotationGroupLabel: string diff --git a/yarn.lock b/yarn.lock index 6bfbe4e..c692e1e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11715,16 +11715,7 @@ string-natural-compare@^3.0.1: resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -11797,14 +11788,7 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==