diff --git a/changelogs/fragments/7787.yml b/changelogs/fragments/7787.yml new file mode 100644 index 000000000000..7f010c3ddcdc --- /dev/null +++ b/changelogs/fragments/7787.yml @@ -0,0 +1,2 @@ +fix: +- [BUG] Allow user Theme Selection retain theme selection ([#7787](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7787)) \ No newline at end of file diff --git a/src/core/public/ui_settings/__snapshots__/ui_settings_client.test.ts.snap b/src/core/public/ui_settings/__snapshots__/ui_settings_client.test.ts.snap index 5d0b95e2938b..f199c867e87a 100644 --- a/src/core/public/ui_settings/__snapshots__/ui_settings_client.test.ts.snap +++ b/src/core/public/ui_settings/__snapshots__/ui_settings_client.test.ts.snap @@ -82,6 +82,9 @@ Object { "dateFormat": Object { "value": "bar", }, + "theme:enableUserControl": Object { + "value": false, + }, } `; @@ -94,6 +97,9 @@ Object { "dateFormat": Object { "value": "Browser", }, + "theme:enableUserControl": Object { + "value": false, + }, } `; @@ -111,6 +117,9 @@ Object { "userValue": "foo", "value": "bar", }, + "theme:enableUserControl": Object { + "value": false, + }, } `; @@ -124,6 +133,9 @@ Object { "userValue": "foo", "value": "Browser", }, + "theme:enableUserControl": Object { + "value": false, + }, } `; @@ -140,6 +152,9 @@ Object { "dateFormat": Object { "value": "bar", }, + "theme:enableUserControl": Object { + "value": false, + }, } `; @@ -153,6 +168,9 @@ Object { "userValue": "foo", "value": "bar", }, + "theme:enableUserControl": Object { + "value": false, + }, } `; diff --git a/src/core/public/ui_settings/types.ts b/src/core/public/ui_settings/types.ts index 86f78443eee6..0a6bd6013ca7 100644 --- a/src/core/public/ui_settings/types.ts +++ b/src/core/public/ui_settings/types.ts @@ -54,6 +54,24 @@ export interface IUiSettingsClient { */ get: (key: string, defaultOverride?: T) => T; + /** + * Gets the value for a specific uiSetting, considering browser-stored settings and advanced settings. + * This method returns an object containing the resolved value and individual setting values. + * + * @param key - The key of the uiSetting to retrieve + * @param defaultOverride - An optional default value to use if the setting is not declared + * @returns An object containing the resolved value and additional setting information + * @throws Error if the setting is not declared and no defaultOverride is provided + */ + getWithBrowserSettings( + key: string, + defaultOverride?: T + ): { + advancedSettingValue: T | undefined; + browserValue: T | undefined; + defaultValue: T; + }; + /** * Gets an observable of the current value for a config key, and all updates to that config * key in the future. Providing a `defaultOverride` argument behaves the same as it does in #get() diff --git a/src/core/public/ui_settings/ui_settings_client.test.ts b/src/core/public/ui_settings/ui_settings_client.test.ts index 9cf4985f440c..6e4378762f06 100644 --- a/src/core/public/ui_settings/ui_settings_client.test.ts +++ b/src/core/public/ui_settings/ui_settings_client.test.ts @@ -34,20 +34,45 @@ import { materialize, take, toArray } from 'rxjs/operators'; import { UiSettingsClient } from './ui_settings_client'; let done$: Subject; +let mockStorage: { [key: string]: string }; -function setup(options: { defaults?: any; initialSettings?: any } = {}) { +function setup(options: { defaults?: any; initialSettings?: any; localStorage?: any } = {}) { const { defaults = { dateFormat: { value: 'Browser' }, aLongNumeral: { value: `${BigInt(Number.MAX_SAFE_INTEGER) + 11n}`, type: 'number' }, + 'theme:enableUserControl': { value: false }, }, initialSettings = {}, + localStorage = {}, } = options; const batchSet = jest.fn(() => ({ settings: {}, })); done$ = new Subject(); + + // Mock localStorage + mockStorage = { ...localStorage }; + const localStorageMock = { + getItem: jest.fn((key) => mockStorage[key]), + setItem: jest.fn((key, value) => { + mockStorage[key] = value.toString(); + }), + removeItem: jest.fn((key) => { + delete mockStorage[key]; + }), + clear: jest.fn(() => { + Object.keys(mockStorage).forEach((key) => delete mockStorage[key]); + }), + }; + Object.defineProperty(window, 'localStorage', { value: localStorageMock }); + + // Initialize localStorage with provided values + if (localStorage.uiSettings) { + window.localStorage.setItem('uiSettings', localStorage.uiSettings); + } + const client = new UiSettingsClient({ defaults, initialSettings, @@ -57,11 +82,88 @@ function setup(options: { defaults?: any; initialSettings?: any } = {}) { done$, }); - return { client, batchSet }; + return { client, batchSet, localStorage: localStorageMock }; } +beforeEach(() => { + mockStorage = {}; +}); + afterEach(() => { done$.complete(); + window.localStorage.clear(); + jest.resetAllMocks(); +}); + +describe('#getWithBrowserSettings', () => { + beforeEach(() => { + window.localStorage.clear(); + }); + + afterEach(() => { + window.localStorage.clear(); + done$.complete(); + jest.resetAllMocks(); + }); + + it('returns correct values when user control is enabled', () => { + const { client } = setup({ + defaults: { + 'theme:enableUserControl': { value: true }, + testSetting: { value: 'defaultValue' }, + }, + initialSettings: { + testSetting: { userValue: 'advancedValue' }, + }, + localStorage: { + uiSettings: JSON.stringify({ + testSetting: { userValue: 'browserValue' }, + }), + }, + }); + + const result = client.getWithBrowserSettings('testSetting'); + expect(result).toEqual({ + advancedSettingValue: 'advancedValue', + browserValue: 'browserValue', + defaultValue: 'defaultValue', + }); + }); + + it('returns correct values when user control is disabled', () => { + const { client } = setup({ + defaults: { + 'theme:enableUserControl': { value: false }, + testSetting: { value: 'defaultValue' }, + }, + initialSettings: { + testSetting: { userValue: 'advancedValue' }, + }, + }); + + const result = client.getWithBrowserSettings('testSetting'); + expect(result).toEqual({ + advancedSettingValue: 'advancedValue', + browserValue: undefined, + defaultValue: 'defaultValue', + }); + }); + + it('returns correct values for a setting with only a default value', () => { + const { client } = setup({ + defaults: { + 'theme:enableUserControl': { value: true }, + testSetting: { value: 'defaultValue' }, + }, + }); + + const result = client.getWithBrowserSettings('testSetting'); + expect(result).toEqual({ + advancedSettingValue: undefined, + browserValue: undefined, + defaultValue: 'defaultValue', + }); + }); }); describe('#getDefault', () => { diff --git a/src/core/public/ui_settings/ui_settings_client.ts b/src/core/public/ui_settings/ui_settings_client.ts index 31a38e75ca19..b3a50259c706 100644 --- a/src/core/public/ui_settings/ui_settings_client.ts +++ b/src/core/public/ui_settings/ui_settings_client.ts @@ -52,17 +52,18 @@ export class UiSettingsClient implements IUiSettingsClient { private readonly api: UiSettingsApi; private readonly defaults: Record; private cache: Record; + private browserStoredSettings: Record = {}; constructor(params: UiSettingsClientParams) { this.api = params.api; this.defaults = cloneDeep(params.defaults); this.cache = defaultsDeep({}, this.defaults, cloneDeep(params.initialSettings)); - if ( + const userControl = this.cache['theme:enableUserControl']?.userValue ?? - this.cache['theme:enableUserControl']?.value - ) { - this.cache = defaultsDeep(this.cache, this.getBrowserStoredSettings()); + this.cache['theme:enableUserControl']?.value; + if (userControl) { + this.browserStoredSettings = this.getBrowserStoredSettings(); } params.done$.subscribe({ @@ -114,6 +115,43 @@ You can use \`IUiSettingsClient.get("${key}", defaultValue)\`, which will just r return this.resolveValue(value, type); } + getWithBrowserSettings( + key: string, + defaultOverride?: T + ): { + advancedSettingValue: T | undefined; + browserValue: T | undefined; + defaultValue: T; + } { + const declared = this.isDeclared(key); + + if (!declared && defaultOverride === undefined) { + throw new Error( + `Unexpected \`IUiSettingsClient.getWithBrowserSettings("${key}")\` call on unrecognized configuration setting "${key}".` + ); + } + + const type = this.cache[key]?.type; + const advancedSettingValue = this.cache[key]?.userValue; + const browserValue = this.browserStoredSettings[key]?.userValue; + const defaultValue = defaultOverride !== undefined ? defaultOverride : this.cache[key]?.value; + + // Resolve the value based on the type + const resolveValue = (val: any): T => this.resolveValue(val, type); + + const resolvedAdvancedValue = + advancedSettingValue !== undefined ? resolveValue(advancedSettingValue) : undefined; + const resolvedBrowserValue = + browserValue !== undefined ? resolveValue(browserValue) : undefined; + const resolvedDefaultValue = resolveValue(defaultValue); + + return { + advancedSettingValue: resolvedAdvancedValue, + browserValue: resolvedBrowserValue, + defaultValue: resolvedDefaultValue, + }; + } + get$(key: string, defaultOverride?: T) { return concat( defer(() => of(this.get(key, defaultOverride))), @@ -230,8 +268,15 @@ You can use \`IUiSettingsClient.get("${key}", defaultValue)\`, which will just r const declared = this.isDeclared(key); const defaults = this.defaults; - - const oldVal = declared ? this.cache[key].userValue : undefined; + const userControl = + this.cache['theme:enableUserControl']?.userValue ?? + this.cache['theme:enableUserControl']?.value; + const oldVal = userControl + ? this.getBrowserStoredSettings()[key]?.userValue ?? + (declared ? this.cache[key].userValue : undefined) + : declared + ? this.cache[key].userValue + : undefined; const unchanged = oldVal === newVal; if (unchanged) { @@ -242,16 +287,15 @@ You can use \`IUiSettingsClient.get("${key}", defaultValue)\`, which will just r this.setLocally(key, newVal); try { - if ( - this.cache['theme:enableUserControl']?.userValue ?? - this.cache['theme:enableUserControl']?.value - ) { - const { settings } = this.cache[key]?.preferBrowserSetting - ? this.setBrowserStoredSettings(key, newVal) - : (await this.api.batchSet(key, newVal)) || {}; - this.cache = defaultsDeep({}, defaults, this.getBrowserStoredSettings(), settings); + if (userControl) { + if (this.cache[key]?.preferBrowserSetting) { + this.setBrowserStoredSettings(key, newVal); + } else { + const { settings = {} } = (await this.api.batchSet(key, newVal)) || {}; + this.cache = defaultsDeep({}, this.cache, settings); + } } else { - const { settings } = (await this.api.batchSet(key, newVal)) || {}; + const { settings = {} } = (await this.api.batchSet(key, newVal)) || {}; this.cache = defaultsDeep({}, defaults, settings); } this.saved$.next({ key, newValue: newVal, oldValue: initialVal }); diff --git a/src/core/public/ui_settings/ui_settings_service.mock.ts b/src/core/public/ui_settings/ui_settings_service.mock.ts index 231627fa53bd..56e69b551b34 100644 --- a/src/core/public/ui_settings/ui_settings_service.mock.ts +++ b/src/core/public/ui_settings/ui_settings_service.mock.ts @@ -49,6 +49,7 @@ const createSetupContractMock = () => { getUpdate$: jest.fn(), getSaved$: jest.fn(), getUpdateErrors$: jest.fn(), + getWithBrowserSettings: jest.fn(), }; setupContract.get$.mockReturnValue(new Rx.Subject()); setupContract.getUpdate$.mockReturnValue(new Rx.Subject()); diff --git a/src/core/server/ui_settings/settings/theme.ts b/src/core/server/ui_settings/settings/theme.ts index 3f0af93aa5b6..8e7813943003 100644 --- a/src/core/server/ui_settings/settings/theme.ts +++ b/src/core/server/ui_settings/settings/theme.ts @@ -47,7 +47,7 @@ export const getThemeSettings = (): Record => { name: i18n.translate('core.ui_settings.params.enableUserControlTitle', { defaultMessage: 'Enable user control', }), - value: true, + value: false, description: i18n.translate('core.ui_settings.params.enableUserControlText', { defaultMessage: `Enable users to control theming and dark or light mode via "Appearance" control in top navigation. When true, those settings can no longer be set globally by administrators.`, }), diff --git a/src/plugins/advanced_settings/public/header_user_theme_menu.tsx b/src/plugins/advanced_settings/public/header_user_theme_menu.tsx index 86a78cbcc2d2..eb9c68bb341c 100644 --- a/src/plugins/advanced_settings/public/header_user_theme_menu.tsx +++ b/src/plugins/advanced_settings/public/header_user_theme_menu.tsx @@ -50,21 +50,32 @@ export const HeaderUserThemeMenu = () => { const defaultScreenMode = uiSettings.getDefault('theme:darkMode'); const prefersAutomatic = (window.localStorage.getItem('useBrowserColorScheme') && window.matchMedia) || false; + + const [enableUserControl] = useUiSetting$('theme:enableUserControl'); const [darkMode, setDarkMode] = useUiSetting$('theme:darkMode'); const [themeVersion, setThemeVersion] = useUiSetting$('theme:version'); const [isPopoverOpen, setPopover] = useState(false); // TODO: improve naming? - const [theme, setTheme] = useState( - themeOptions.find((t) => t.value === themeVersionValueMap[themeVersion])?.value || - themeVersionValueMap[defaultTheme] - ); - const [screenMode, setScreenMode] = useState( - prefersAutomatic - ? screenModeOptions[2].value - : darkMode - ? screenModeOptions[1].value - : screenModeOptions[0].value - ); + + const [theme, setTheme] = useState(() => { + const result = uiSettings.getWithBrowserSettings('theme:version'); + const currentTheme = enableUserControl + ? result.browserValue ?? result.advancedSettingValue ?? result.defaultValue + : result.advancedSettingValue ?? result.defaultValue; + return ( + themeOptions.find((t) => t.value === themeVersionValueMap[currentTheme])?.value ?? + themeVersionValueMap[result.defaultValue] + ); + }); + + const [screenMode, setScreenMode] = useState(() => { + if (prefersAutomatic) return 'automatic'; + const result = uiSettings.getWithBrowserSettings('theme:darkMode'); + const currentDarkMode = enableUserControl + ? result.browserValue ?? result.advancedSettingValue ?? result.defaultValue + : result.advancedSettingValue ?? result.defaultValue; + return currentDarkMode ? 'dark' : 'light'; + }); const legacyAppearance = !uiSettings.get('home:useNewHomePage'); @@ -82,6 +93,10 @@ export const HeaderUserThemeMenu = () => { const onAppearanceSubmit = async (e: SyntheticEvent) => { const actions = [setThemeVersion(themeOptions.find((t) => theme === t.value)?.value ?? '')]; + const result = uiSettings.getWithBrowserSettings('theme:darkMode'); + const currentDarkMode = enableUserControl + ? result.browserValue ?? result.advancedSettingValue ?? result.defaultValue + : result.advancedSettingValue ?? result.defaultValue; if (screenMode === 'automatic') { const browserMode = window.matchMedia('(prefers-color-scheme: dark)').matches; @@ -90,7 +105,7 @@ export const HeaderUserThemeMenu = () => { if (browserMode !== darkMode) { actions.push(setDarkMode(browserMode)); } - } else if ((screenMode === 'dark') !== darkMode) { + } else if ((screenMode === 'dark') !== currentDarkMode) { actions.push(setDarkMode(screenMode === 'dark')); window.localStorage.removeItem('useBrowserColorScheme'); } else { diff --git a/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap b/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap index 637852c448e3..e8cbcceaabec 100644 --- a/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap +++ b/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap @@ -933,6 +933,7 @@ exports[`Dashboard top nav render in embed mode 1`] = ` "getSaved$": [MockFunction], "getUpdate$": [MockFunction], "getUpdateErrors$": [MockFunction], + "getWithBrowserSettings": [MockFunction], "isCustom": [MockFunction], "isDeclared": [MockFunction], "isDefault": [MockFunction], @@ -1999,6 +2000,7 @@ exports[`Dashboard top nav render in embed mode, and force hide filter bar 1`] = "getSaved$": [MockFunction], "getUpdate$": [MockFunction], "getUpdateErrors$": [MockFunction], + "getWithBrowserSettings": [MockFunction], "isCustom": [MockFunction], "isDeclared": [MockFunction], "isDefault": [MockFunction], @@ -3065,6 +3067,7 @@ exports[`Dashboard top nav render in embed mode, components can be forced show b "getSaved$": [MockFunction], "getUpdate$": [MockFunction], "getUpdateErrors$": [MockFunction], + "getWithBrowserSettings": [MockFunction], "isCustom": [MockFunction], "isDeclared": [MockFunction], "isDefault": [MockFunction], @@ -4131,6 +4134,7 @@ exports[`Dashboard top nav render in full screen mode with appended URL param bu "getSaved$": [MockFunction], "getUpdate$": [MockFunction], "getUpdateErrors$": [MockFunction], + "getWithBrowserSettings": [MockFunction], "isCustom": [MockFunction], "isDeclared": [MockFunction], "isDefault": [MockFunction], @@ -5197,6 +5201,7 @@ exports[`Dashboard top nav render in full screen mode, no componenets should be "getSaved$": [MockFunction], "getUpdate$": [MockFunction], "getUpdateErrors$": [MockFunction], + "getWithBrowserSettings": [MockFunction], "isCustom": [MockFunction], "isDeclared": [MockFunction], "isDefault": [MockFunction], @@ -6263,6 +6268,7 @@ exports[`Dashboard top nav render with all components 1`] = ` "getSaved$": [MockFunction], "getUpdate$": [MockFunction], "getUpdateErrors$": [MockFunction], + "getWithBrowserSettings": [MockFunction], "isCustom": [MockFunction], "isDeclared": [MockFunction], "isDefault": [MockFunction], diff --git a/src/plugins/dashboard/public/application/embeddable/empty/__snapshots__/dashboard_empty_screen.test.tsx.snap b/src/plugins/dashboard/public/application/embeddable/empty/__snapshots__/dashboard_empty_screen.test.tsx.snap index 9889462a0472..6f51eb7cca8a 100644 --- a/src/plugins/dashboard/public/application/embeddable/empty/__snapshots__/dashboard_empty_screen.test.tsx.snap +++ b/src/plugins/dashboard/public/application/embeddable/empty/__snapshots__/dashboard_empty_screen.test.tsx.snap @@ -177,6 +177,7 @@ exports[`DashboardEmptyScreen renders correctly with readonly mode 1`] = ` "getSaved$": [MockFunction], "getUpdate$": [MockFunction], "getUpdateErrors$": [MockFunction], + "getWithBrowserSettings": [MockFunction], "isCustom": [MockFunction], "isDeclared": [MockFunction], "isDefault": [MockFunction], @@ -525,6 +526,7 @@ exports[`DashboardEmptyScreen renders correctly with visualize paragraph 1`] = ` "getSaved$": [MockFunction], "getUpdate$": [MockFunction], "getUpdateErrors$": [MockFunction], + "getWithBrowserSettings": [MockFunction], "isCustom": [MockFunction], "isDeclared": [MockFunction], "isDefault": [MockFunction], @@ -912,6 +914,7 @@ exports[`DashboardEmptyScreen renders correctly without visualize paragraph 1`] "getSaved$": [MockFunction], "getUpdate$": [MockFunction], "getUpdateErrors$": [MockFunction], + "getWithBrowserSettings": [MockFunction], "isCustom": [MockFunction], "isDeclared": [MockFunction], "isDefault": [MockFunction], diff --git a/src/plugins/data/public/ui/query_editor/__snapshots__/language_selector.test.tsx.snap b/src/plugins/data/public/ui/query_editor/__snapshots__/language_selector.test.tsx.snap index 41755d0462a8..8b2f6ac2add9 100644 --- a/src/plugins/data/public/ui/query_editor/__snapshots__/language_selector.test.tsx.snap +++ b/src/plugins/data/public/ui/query_editor/__snapshots__/language_selector.test.tsx.snap @@ -469,6 +469,7 @@ exports[`LanguageSelector should select DQL if language is kuery 1`] = ` "getSaved$": [MockFunction], "getUpdate$": [MockFunction], "getUpdateErrors$": [MockFunction], + "getWithBrowserSettings": [MockFunction], "isCustom": [MockFunction], "isDeclared": [MockFunction], "isDefault": [MockFunction], @@ -1037,6 +1038,7 @@ exports[`LanguageSelector should select lucene if language is lucene 1`] = ` "getSaved$": [MockFunction], "getUpdate$": [MockFunction], "getUpdateErrors$": [MockFunction], + "getWithBrowserSettings": [MockFunction], "isCustom": [MockFunction], "isDeclared": [MockFunction], "isDefault": [MockFunction], diff --git a/yarn.lock b/yarn.lock index e3cacd0f1623..a8d9af23c63e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15986,7 +15986,7 @@ string-similarity@^4.0.1: resolved "https://registry.yarnpkg.com/string-similarity/-/string-similarity-4.0.4.tgz#42d01ab0b34660ea8a018da8f56a3309bb8b2a5b" integrity sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ== -"string-width-cjs@npm:string-width@^4.2.0": +"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== @@ -16021,15 +16021,6 @@ string-width@^3.0.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.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== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -16108,7 +16099,7 @@ stringify-entities@^3.0.1: character-entities-legacy "^1.0.0" xtend "^4.0.0" -"strip-ansi-cjs@npm: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== @@ -16150,13 +16141,6 @@ strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -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== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -18300,7 +18284,7 @@ workerpool@6.2.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -18326,15 +18310,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"