diff --git a/webapp/packages/core-blocks/src/ResourcesHooks/useResource.ts b/webapp/packages/core-blocks/src/ResourcesHooks/useResource.ts index ac7c640f80..43f4f455ad 100644 --- a/webapp/packages/core-blocks/src/ResourcesHooks/useResource.ts +++ b/webapp/packages/core-blocks/src/ResourcesHooks/useResource.ts @@ -161,7 +161,7 @@ export function useResource< propertiesRef.includes = includes; } - if (key === null || propertiesRef.key === null || !propertiesRef.resource.isIntersect(key, propertiesRef.key)) { + if (key === null || propertiesRef.key === null || !propertiesRef.resource.isEqual(key, propertiesRef.key)) { propertiesRef.key = key; } }); @@ -209,7 +209,7 @@ export function useResource< key = toJS(key); if (this.useRef[0] !== null && propertiesRef.resource.useTracker.hasUseId(this.useRef[1])) { - if (key !== null && propertiesRef.resource.isIntersect(key, this.useRef[0])) { + if (key !== null && propertiesRef.resource.isEqual(key, this.useRef[0])) { return; } diff --git a/webapp/packages/core-resource/src/Resource/Resource.ts b/webapp/packages/core-resource/src/Resource/Resource.ts index 49a37297b6..0e451dac95 100644 --- a/webapp/packages/core-resource/src/Resource/Resource.ts +++ b/webapp/packages/core-resource/src/Resource/Resource.ts @@ -44,6 +44,7 @@ export abstract class Resource< super(); this.isKeyEqual = this.isKeyEqual.bind(this); this.isIntersect = this.isIntersect.bind(this); + this.isEqual = this.isEqual.bind(this); this.logger = new ResourceLogger(this.getName()); this.aliases = new ResourceAliases(this.logger, this.validateKey.bind(this)); @@ -107,6 +108,35 @@ export abstract class Resource< return ResourceKeyUtils.isIntersect(key, nextKey, this.isKeyEqual); } + /** + * Checks + * @param param - Resource key + * @param second - Resource key + * @returns {boolean} Returns true if key can is the same by all key-values + */ + isEqual(param: ResourceKey, second: ResourceKey): boolean { + if (param === second) { + return true; + } + + if (isResourceAlias(param) && isResourceAlias(second)) { + param = this.aliases.transformToAlias(param); + second = this.aliases.transformToAlias(second); + + return param.isEqual(second) && this.isEqual(param.target, second.target); + } + + if (isResourceAlias(param) || isResourceAlias(second)) { + return false; + } + + if (isResourceKeyList(param) && isResourceKeyList(second)) { + return param.isEqual(second, this.isKeyEqual); + } + + return ResourceKeyUtils.isEqual(param, second, this.isKeyEqual); + } + /** * Can be overridden to provide equality check for complicated keys */ diff --git a/webapp/packages/core-resource/src/Resource/ResourceKeyList.ts b/webapp/packages/core-resource/src/Resource/ResourceKeyList.ts index 2831517a04..578cc067e8 100644 --- a/webapp/packages/core-resource/src/Resource/ResourceKeyList.ts +++ b/webapp/packages/core-resource/src/Resource/ResourceKeyList.ts @@ -5,12 +5,17 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ +import { isArraysEqual } from '@cloudbeaver/core-utils'; export class ResourceKeyList extends Array { static get [Symbol.species]() { return Array; } + isEqual(key: ResourceKeyList, isEqual?: (a: TKey, b: TKey) => boolean): boolean { + return isArraysEqual(this, key, isEqual, true); + } + includes(key: TKey, fromIndex?: number): boolean; includes(key: TKey | ResourceKeyList): boolean; includes(key: TKey | ResourceKeyList, isEqual: (keyA: TKey, keyB: TKey) => boolean): boolean; diff --git a/webapp/packages/core-resource/src/Resource/ResourceKeyUtils.ts b/webapp/packages/core-resource/src/Resource/ResourceKeyUtils.ts index 4a7f0eb131..284e3b041e 100644 --- a/webapp/packages/core-resource/src/Resource/ResourceKeyUtils.ts +++ b/webapp/packages/core-resource/src/Resource/ResourceKeyUtils.ts @@ -37,6 +37,11 @@ export interface ResourceKeyUtils { second: TKey | ResourceKeyList, isEqual?: (keyA: TKey, keyB: TKey) => boolean, ) => boolean; + isEqual: ( + first: TKey | ResourceKeyList, + second: TKey | ResourceKeyList, + isEqualFn?: (keyA: TKey, keyB: TKey) => boolean, + ) => boolean; join: (...keys: Array>) => ResourceKeyList; toArray: (key: TKey | ResourceKeyList) => TKey[]; toList: (key: TKey | ResourceKeyList) => ResourceKeyList; @@ -125,6 +130,25 @@ export const ResourceKeyUtils: ResourceKeyUtils = { return isEqual(param, key); }, + isEqual( + param: TKey | ResourceKeyList, + key: TKey | ResourceKeyList, + isEqualFn = (keyA: TKey, keyB: TKey) => keyA === keyB, + ): boolean { + if (param === key) { + return true; + } + + if (isResourceKeyList(param) && isResourceKeyList(key)) { + return param.isEqual(key, isEqualFn); + } + + if (isResourceKeyList(key) || isResourceKeyList(param)) { + return false; + } + + return isEqualFn(param, key); + }, join(...keys: Array>): ResourceKeyList { const list: TKey[] = []; diff --git a/webapp/packages/core-utils/src/isArraysEqual.test.ts b/webapp/packages/core-utils/src/isArraysEqual.test.ts index 92e4b5fb41..cd2b78e3a2 100644 --- a/webapp/packages/core-utils/src/isArraysEqual.test.ts +++ b/webapp/packages/core-utils/src/isArraysEqual.test.ts @@ -55,4 +55,22 @@ describe('Is array equals', () => { test('should use isEqual argument if passed and "order" argument is "true"', () => { expect(isArraysEqual([{ a: 1 }], [{ a: 1 }], (a, b) => a.a === b.a, true)).toBe(true); }); + + test('should not pass with no equal fn and array of objects (length > 1)', () => { + expect(isArraysEqual([{ b: 3 }, { a: 1 }], [{ a: 1 }, { b: 3 }])).toBe(false); + }); + + test('should not pass with no equal fn and primitive and non primitive in array', () => { + expect(isArraysEqual([1, 1, { a: 1 }, 2], [2, { a: 1 }, 1, 1])).toBe(false); + }); + + test('should pass with equal fn and array of objects (length > 1)', () => { + const isEqual = jest.fn((a: { a: number }, b: { a: number }) => a.a === b.a); + expect(isArraysEqual([{ a: 3 }, { a: 1 }], [{ a: 1 }, { a: 3 }], isEqual)).toBe(true); + }); + + test('should pass with equal fn and primitive and non primitive in array', () => { + const isEqual = jest.fn((a: { a: number }, b: { a: number }) => a.a === b.a); + expect(isArraysEqual([1, { a: 3 }, { a: 1 }] as any, [1, { a: 1 }, { a: 3 }] as any, isEqual)).toBe(true); + }); }); diff --git a/webapp/packages/core-utils/src/isArraysEqual.ts b/webapp/packages/core-utils/src/isArraysEqual.ts index 686eb19c76..975f8f5fdf 100644 --- a/webapp/packages/core-utils/src/isArraysEqual.ts +++ b/webapp/packages/core-utils/src/isArraysEqual.ts @@ -5,6 +5,7 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ +import { isPrimitive } from './isPrimitive'; const isObjectEqual = (a: T, b: T) => a === b; @@ -13,11 +14,49 @@ export function isArraysEqual(first: T[], second: T[], isEqual: (a: T, b: T) return false; } - return !first.some((b, index) => { - if (order) { - return !isEqual(second[index], b); + const map = new Map(); + + for (let i = 0; i < first.length; i++) { + const currentFirst = first[i]; + const currentSecond = second[i]; + + if (order && !isEqual(currentFirst, currentSecond)) { + return false; } - return !second.some(a => isEqual(a, b)); - }); + map.set(currentFirst, (map.get(currentFirst) ?? 0) + 1); + } + + if (order) { + return true; + } + + for (let i = 0; i < second.length; i++) { + const currentSecond = second[i]; + const isPrimitiveValue = isPrimitive(currentSecond); + + if (!isPrimitiveValue) { + for (let j = 0; j < first.length; j++) { + if (isEqual(first[j], currentSecond)) { + map.set(currentSecond, Number(map.get(currentSecond)) - 1); + break; + } + + if (j === first.length - 1) { + return false; + } + } + continue; + } + + const mapValue = map.get(currentSecond); + + if (mapValue === undefined || mapValue <= 0) { + return false; + } + + map.set(currentSecond, Number(map.get(currentSecond)) - 1); + } + + return true; } diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.m.css b/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.m.css new file mode 100644 index 0000000000..a9c02f1af7 --- /dev/null +++ b/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.m.css @@ -0,0 +1,4 @@ +.form { + flex: 1; + overflow: auto; +} diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx index 3d5a35a408..b8727b3623 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx @@ -7,7 +7,6 @@ */ import { observer } from 'mobx-react-lite'; import { useCallback, useRef } from 'react'; -import styled, { css } from 'reshadow'; import { AUTH_PROVIDER_LOCAL_ID, EAdminPermission } from '@cloudbeaver/core-authentication'; import { @@ -24,11 +23,13 @@ import { ObjectPropertyInfoForm, Radio, RadioGroup, + s, Textarea, useAdministrationSettings, useFormValidator, usePermission, useResource, + useS, useTranslate, } from '@cloudbeaver/core-blocks'; import { DatabaseAuthModelsResource, DBDriverResource, isLocalConnection } from '@cloudbeaver/core-connections'; @@ -43,19 +44,13 @@ import { ProjectSelect } from '@cloudbeaver/plugin-projects'; import { ConnectionFormService } from '../ConnectionFormService'; import type { IConnectionFormProps } from '../IConnectionFormProps'; import { ConnectionOptionsTabService } from './ConnectionOptionsTabService'; +import styles from './Options.m.css'; import { ParametersForm } from './ParametersForm'; import { ProviderPropertiesForm } from './ProviderPropertiesForm'; import { useOptions } from './useOptions'; const PROFILE_AUTH_MODEL_ID = 'profile'; -const styles = css` - Form { - flex: 1; - overflow: auto; - } -`; - interface IDriverConfiguration { name: string; value: DriverConfigurationType; @@ -81,6 +76,7 @@ export const Options: TabContainerPanelComponent = observe const formRef = useRef(null); const translate = useTranslate(); const { info, config, availableDrivers, submittingTask: submittingHandlers, disabled } = state; + const style = useS(styles); //@TODO it's here until the profile implementation in the CloudBeaver const readonly = state.readonly || info?.authModel === PROFILE_AUTH_MODEL_ID; @@ -138,7 +134,7 @@ export const Options: TabContainerPanelComponent = observe const { data: applicableAuthModels } = useResource( Options, DatabaseAuthModelsResource, - getComputed(() => (driver?.applicableAuthModels ? resourceKeyList(driver.applicableAuthModels) : CachedResourceListEmptyKey)), + driver?.applicableAuthModels ? resourceKeyList(driver.applicableAuthModels) : CachedResourceListEmptyKey, ); const { data: authModel } = useResource( @@ -167,8 +163,8 @@ export const Options: TabContainerPanelComponent = observe properties = info.authProperties; } - return styled(styles)( -
+ return ( + @@ -354,6 +350,6 @@ export const Options: TabContainerPanelComponent = observe )} -
, + ); });