diff --git a/__tests__/components/AccountMultisigGraph.spec.ts b/__tests__/components/AccountMultisigGraph.spec.ts index 2784849ee..08ac2ce43 100644 --- a/__tests__/components/AccountMultisigGraph.spec.ts +++ b/__tests__/components/AccountMultisigGraph.spec.ts @@ -18,7 +18,7 @@ import { AccountMultisigGraphTs } from '@/components/AccountMultisigGraph/Accoun import { AccountModel } from '@/core/database/entities/AccountModel'; import { AccountState } from '@/store/Account'; import { getComponent } from '@MOCKS/Components'; -import { MultisigAccountInfoDTO } from 'symbol-openapi-typescript-fetch-client'; +import { AddressBook } from 'symbol-address-book'; import { Address, MultisigAccountInfo } from 'symbol-sdk'; describe('components/AccountMultisigGraph', () => { @@ -27,105 +27,67 @@ describe('components/AccountMultisigGraph', () => { const cosig2Address = 'TD2KY5BKECF6YKSWSTGMUHFP66GG33S5MG6UOYQ'; const cosig11Address = 'TDYEWBAMEW27OODEODPQ4OKYFEU6N24NC4O7KZA'; const cosig21Address = 'TCZCGIGFXU5LW7BCUQCXVVYS6JJDHJWONYVVGXY'; + const randomAddress = 'TCZ1234FXU5LW7BCUQCXVVYS6JJDHJWONYVVGXY'; + const mockAddressBook = new AddressBook([ + { + id: '0', + name: 'contact name', + address: cosig2Address, + }, + ]); - const multisigAccountGraphInfosDTO = [ + const multisigAccounts = [ + { + version: 1, + accountAddress: Address.createFromRawAddress(msigAddress), + minApproval: 2, + minRemoval: 1, + cosignatoryAddresses: [Address.createFromRawAddress(cosig1Address), Address.createFromRawAddress(cosig2Address)], + multisigAddresses: [], + }, + { + version: 1, + accountAddress: Address.createFromRawAddress(cosig1Address), + minApproval: 1, + minRemoval: 1, + cosignatoryAddresses: [Address.createFromRawAddress(cosig11Address)], + multisigAddresses: [Address.createFromRawAddress(msigAddress)], + }, { - level: 0, - multisigEntries: [ - { - multisig: { - version: 1, - accountAddress: msigAddress, - minApproval: 2, - minRemoval: 1, - cosignatoryAddresses: [cosig1Address, cosig2Address], - multisigAddresses: [], - }, - }, - ], + version: 1, + accountAddress: Address.createFromRawAddress(cosig2Address), + minApproval: 1, + minRemoval: 1, + cosignatoryAddresses: [Address.createFromRawAddress(cosig21Address)], + multisigAddresses: [Address.createFromRawAddress(msigAddress)], }, { - level: 1, - multisigEntries: [ - { - multisig: { - version: 1, - accountAddress: cosig1Address, - minApproval: 1, - minRemoval: 1, - cosignatoryAddresses: [cosig11Address], - multisigAddresses: [msigAddress], - }, - }, - { - multisig: { - version: 1, - accountAddress: cosig2Address, - minApproval: 1, - minRemoval: 1, - cosignatoryAddresses: [cosig21Address], - multisigAddresses: [msigAddress], - }, - }, - ], + version: 1, + accountAddress: Address.createFromRawAddress(cosig21Address), + minApproval: 0, + minRemoval: 0, + cosignatoryAddresses: [], + multisigAddresses: [Address.createFromRawAddress(cosig2Address)], }, { - level: 2, - multisigEntries: [ - { - multisig: { - version: 1, - accountAddress: cosig21Address, - minApproval: 0, - minRemoval: 0, - cosignatoryAddresses: [], - multisigAddresses: [cosig2Address], - }, - }, - { - multisig: { - version: 1, - accountAddress: cosig11Address, - minApproval: 0, - minRemoval: 0, - cosignatoryAddresses: [], - multisigAddresses: [cosig1Address], - }, - }, - ], + version: 1, + accountAddress: Address.createFromRawAddress(cosig11Address), + minApproval: 0, + minRemoval: 0, + cosignatoryAddresses: [], + multisigAddresses: [Address.createFromRawAddress(cosig1Address), Address.createFromRawAddress(randomAddress)], }, ]; - const toMultisigAccountInfo = (dto: MultisigAccountInfoDTO): MultisigAccountInfo => { - return new MultisigAccountInfo( - dto.multisig.version || 1, - Address.createFromRawAddress(dto.multisig.accountAddress), - dto.multisig.minApproval, - dto.multisig.minRemoval, - dto.multisig.cosignatoryAddresses.map((cosigner) => Address.createFromRawAddress(cosigner)), - dto.multisig.multisigAddresses.map((multisig) => Address.createFromRawAddress(multisig)), - ); - }; - const multisigAccounts = new Map(); - - multisigAccountGraphInfosDTO.map((multisigAccountGraphInfoDTO) => { - multisigAccounts.set( - multisigAccountGraphInfoDTO.level, - multisigAccountGraphInfoDTO.multisigEntries.map((multisigAccountInfoDTO) => { - return toMultisigAccountInfo(multisigAccountInfoDTO); - }), - ); - }); - const mockAccountStore = { namespaced: true, state: { - multisigAccountGraph: undefined, + multisigAccountGraphInfo: undefined, knownAccounts: [], }, getters: { - multisigAccountGraph: (state) => { - return state.multisigAccountGraph; + multisigAccountGraphInfo: (state) => { + return state.multisigAccountGraphInfo; }, knownAccounts: (state) => { return state.knownAccounts; @@ -133,11 +95,26 @@ describe('components/AccountMultisigGraph', () => { }, }; - const getAccountMultisigGraphWrapper = (account: AccountModel, stateChanges?: AccountState) => { - const wrapper = getComponent(AccountMultisigGraph, { account: mockAccountStore }, stateChanges, { + const mockAddressBookStore = { + namespaced: true, + getters: { + getAddressBook: () => { + return mockAddressBook; + }, + }, + }; + + const mockStore = { + account: mockAccountStore, + addressBook: mockAddressBookStore, + }; + + const getAccountMultisigGraphWrapper = (account: AccountModel, stateChanges?: AccountState, stubs?: Record) => { + const props = { account, - }); - return wrapper; + }; + + return getComponent(AccountMultisigGraph, mockStore, stateChanges, props, stubs); }; test('renders account multisig graph component when multisig account graph is undefined', () => { @@ -165,26 +142,80 @@ describe('components/AccountMultisigGraph', () => { expect(wrapper.find('span.label').exists()).toBeTruthy(); }); - test('current account is selected in multisig graph', () => { + test('current account is selected in multisig graph', async () => { // Arrange: const account = ({ address: cosig1Address } as unknown) as AccountModel; const stateChanges = { - multisigAccountGraph: (multisigAccounts as unknown) as MultisigAccountInfo[][], + multisigAccountGraphInfo: (multisigAccounts as unknown) as MultisigAccountInfo[], knownAccounts: [{ name: 'myAccount', address: cosig1Address }], } as Partial; // Act: const wrapper = getAccountMultisigGraphWrapper(account, stateChanges as AccountState); const component = wrapper.vm as AccountMultisigGraphTs; + await component.$nextTick(); + + // Assert: + expect(component.dataset[0].title).toContain(msigAddress); + expect(component.dataset[0].children[0].selected).toBe(true); + }); + + test('center graph view', () => { + // Arrange: + const account = ({ address: cosig1Address } as unknown) as AccountModel; + const stateChanges = { + multisigAccountGraph: [], + } as Partial; + const stubs = { + VueTree: { + template: '
', + }, + }; + const mockSetAttribute = jest.fn(); + const mockRemToPixels = jest.fn().mockReturnValue(2); + const expectedStyles = `transform: scale(1) translate(2px, 0px); transform-origin: center center;`; + + // Act: + const wrapper = getAccountMultisigGraphWrapper(account, stateChanges as AccountState, stubs); + const component = wrapper.vm as AccountMultisigGraphTs; + const vueTreeChildren = (component.$refs['VueTree'] as Vue).$el.children; + vueTreeChildren[0].setAttribute = mockSetAttribute; + vueTreeChildren[1].setAttribute = mockSetAttribute; + component['remToPixels'] = mockRemToPixels; + component['centerGraph'](); + + // Assert: + expect(mockSetAttribute).toBeCalledTimes(2); + expect(mockSetAttribute).nthCalledWith(1, 'style', expectedStyles); + expect(mockSetAttribute).nthCalledWith(2, 'style', expectedStyles); + }); + + test('show graph modal', () => { + // Arrange: + const account = ({ address: cosig1Address } as unknown) as AccountModel; + const stateChanges = { + multisigAccountGraph: [], + } as Partial; + const mockUpdateGraphConfig = jest.fn(); + const mockCenterGraph = jest.fn(); + const stubs = { + VueTree: { + template: '
', + }, + }; + + // Act: + const wrapper = getAccountMultisigGraphWrapper(account, stateChanges as AccountState, stubs); + const graphViewElement = wrapper.find('.graph-view'); + const component = wrapper.vm as AccountMultisigGraphTs; + component['updateGraphConfig'] = mockUpdateGraphConfig; + component['centerGraph'] = mockCenterGraph; + component['showGraphModal'](); // Assert: - expect(component.multisigGraphTree[0].info.accountAddress.address).toBe(msigAddress); - expect(component.multisigGraphTree[0].info.cosignatoryAddresses[0].address).toBe(cosig1Address); - expect(component.multisigGraphTree[0].info.cosignatoryAddresses[1].address).toBe(cosig2Address); - expect(component.multisigGraphTree[0].children[0].info.accountAddress.address).toBe(cosig1Address); - expect(component.multisigGraphTree[0].children[1].info.accountAddress.address).toBe(cosig2Address); - expect(component.multisigGraphTree[0].children[0].children[0].info.accountAddress.address).toBe(cosig11Address); - expect(component.multisigGraphTree[0].children[1].children[0].info.accountAddress.address).toBe(cosig21Address); - expect(component.multisigGraphTree[0].children[0].selected).toBe(true); + expect(mockUpdateGraphConfig).toBeCalledTimes(1); + expect(mockCenterGraph).toBeCalledTimes(1); + expect(component.isGraphModalShown).toBe(true); + expect(graphViewElement.exists()).toBe(true); }); }); diff --git a/__tests__/store/Account.spec.ts b/__tests__/store/Account.spec.ts new file mode 100644 index 000000000..9fb84e4b8 --- /dev/null +++ b/__tests__/store/Account.spec.ts @@ -0,0 +1,150 @@ +/* + * (C) Symbol Contributors 2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +import AccountStore from '@/store/Account'; +import { account1 } from '@MOCKS/Accounts'; +import { MultisigService } from '@/services/MultisigService'; +import { Address, MultisigAccountInfo } from 'symbol-sdk'; + +type MultisigAccountInfoProperties = Omit; + +let commit; +let dispatch; + +beforeEach(() => { + commit = jest.fn(); + dispatch = jest.fn(); +}); + +describe('store/Account', () => { + describe('action "LOAD_MULTISIG_GRAPH"', () => { + const address1 = 'TBY22A6EX73URRW3YVZUX72223RNFCI4QR2C3GY'; + const address2 = 'TALPBVKED63OTOS6LNKFIE4H357MBOQPVGJBLOI'; + const address3 = 'TD2KY5BKECF6YKSWSTGMUHFP66GG33S5MG6UOYQ'; + const address4 = 'TDYEWBAMEW27OODEODPQ4OKYFEU6N24NC4O7KZA'; + const address5 = 'TCZCGIGFXU5LW7BCUQCXVVYS6JJDHJWONYVVGXY'; + + const createMultisigAccountsInfo = (address: string) => ({ + version: 1, + accountAddress: Address.createFromRawAddress(address), + minApproval: 2, + minRemoval: 1, + cosignatoryAddresses: [], + multisigAddresses: [], + }); + + const createMockRepositoryFactory = (promise: jest.Mock>) => { + return { + createMultisigRepository: () => ({ + getMultisigAccountGraphInfo: () => ({ + toPromise: promise, + }), + }), + }; + }; + + const runLoadMultisigGraphTest = async ( + mockMultisigAccountGraphInfoPromise: jest.Mock>, + rootMultisigAccountsInfo: MultisigAccountInfoProperties[], + expectedResult: MultisigAccountInfoProperties[], + expectedMultisigAccountGraph: Map | Array, + expectedMultisigAccountGraphInfo: MultisigAccountInfoProperties[], + ) => { + // Arrange: + const repositoryFactory = createMockRepositoryFactory(mockMultisigAccountGraphInfoPromise); + const getters = { + currentAccountAddress: account1.address, + }; + const rootGetters = { + 'network/repositoryFactory': repositoryFactory, + }; + const storeContext = { + commit, + dispatch, + getters, + rootGetters, + }; + //@ts-ignore + jest.spyOn(MultisigService, 'getMultisigInfoFromMultisigGraphInfo').mockReturnValue(rootMultisigAccountsInfo); + + // Act: + const result = await AccountStore.actions.LOAD_MULTISIG_GRAPH(storeContext); + + // Assert: + expect(commit).toHaveBeenNthCalledWith(1, 'multisigAccountGraph', expectedMultisigAccountGraph); + expect(commit).toHaveBeenNthCalledWith(2, 'multisigAccountGraphInfo', expectedMultisigAccountGraphInfo); + expect(result).toEqual(expectedResult); + }; + + test('load full multisig tree', async () => { + // Arrange: + const multisigAccountsInfo = [createMultisigAccountsInfo(address1)]; + const currentMultisigAccountGraphInfo = { + multisigEntries: new Map([ + [-1, [createMultisigAccountsInfo(address1)]], + [0, [createMultisigAccountsInfo(address2), createMultisigAccountsInfo(address3)]], + ]), + }; + const rootMultisigAccountGraphInfo = { + multisigEntries: new Map([ + [0, [createMultisigAccountsInfo(address4)]], + [1, [createMultisigAccountsInfo(address5)]], + ]), + }; + const fullMultisigAccountGraphInfo = { + multisigEntries: new Map([ + [-1, [createMultisigAccountsInfo(address1), createMultisigAccountsInfo(address4)]], + [0, [createMultisigAccountsInfo(address2), createMultisigAccountsInfo(address3), createMultisigAccountsInfo(address5)]], + ]), + }; + + const mockMultisigAccountGraphInfoPromise = jest + .fn() + .mockResolvedValueOnce(currentMultisigAccountGraphInfo) + .mockResolvedValueOnce(rootMultisigAccountGraphInfo as any); + const rootMultisigAccountsInfo = multisigAccountsInfo; + const expectedResult = multisigAccountsInfo; + const expectedMultisigAccountGraph = fullMultisigAccountGraphInfo.multisigEntries; + const expectedMultisigAccountGraphInfo = multisigAccountsInfo; + + // Act + Assert: + await runLoadMultisigGraphTest( + mockMultisigAccountGraphInfoPromise, + rootMultisigAccountsInfo, + expectedResult, + expectedMultisigAccountGraph, + expectedMultisigAccountGraphInfo, + ); + }); + + test('handle error', async () => { + // Arrange: + const mockMultisigAccountGraphInfoPromise = jest.fn().mockRejectedValue(null); + const rootMultisigAccountsInfo = []; + const expectedResult = []; + const expectedMultisigAccountGraph = []; + const expectedMultisigAccountGraphInfo = []; + + // Act + Assert: + await runLoadMultisigGraphTest( + mockMultisigAccountGraphInfoPromise, + rootMultisigAccountsInfo, + expectedResult, + expectedMultisigAccountGraph, + expectedMultisigAccountGraphInfo, + ); + }); + }); +}); diff --git a/__tests__/views/pages/AccountDetailsPage.spec.ts b/__tests__/views/pages/AccountDetailsPage.spec.ts new file mode 100644 index 000000000..98169bbba --- /dev/null +++ b/__tests__/views/pages/AccountDetailsPage.spec.ts @@ -0,0 +1,131 @@ +/* + * (C) Symbol Contributors 2022 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +import AccountDetailsPage from '@/views/pages/accounts/AccountDetailsPage/AccountDetailsPage.vue'; +import { AccountDetailsPageTs } from '@/views/pages/accounts/AccountDetailsPage/AccountDetailsPageTs'; +import { AccountModel } from '@/core/database/entities/AccountModel'; +import { Signer } from '@/store/Account'; +import { getComponent } from '@MOCKS/Components'; +import { WalletsModel1, account1 } from '@MOCKS/Accounts'; + +const accountDetailsPageTs = new AccountDetailsPageTs(); +type Props = typeof accountDetailsPageTs.$props; + +describe('pages/AccountDetailsPage', () => { + const defaultSigner = { + label: 'label', + address: account1.address, + multisig: false, + requiredCosigApproval: 0, + requiredCosigRemoval: 0, + }; + + const getAccountDetailsPageWrapper = (props: Props, stateChanges?: Record) => { + const mockStore = { + app: { + namespaced: true, + getters: { + defaultAccount: () => account1, + }, + }, + account: { + namespaced: true, + state: { + currentAccount: WalletsModel1, + currentSigner: defaultSigner, + }, + getters: { + currentAccount: (state) => state.currentAccount, + currentSigner: (state) => state.currentSigner, + knownAccounts: () => [], + }, + }, + metadata: { + namespaced: true, + getters: { + accountMetadataList: () => [], + }, + }, + profile: { + namespaced: true, + getters: { + currentProfile: () => WalletsModel1, + }, + }, + }; + + return getComponent(AccountDetailsPage, mockStore, stateChanges, props, {}); + }; + + describe('AccountMultisigGraph', () => { + const runAccountMultisigGraphTest = async ( + currentAccount: AccountModel, + currentSigner: Signer, + shouldComponentBeShown: boolean, + ) => { + // Arrange: + const props = {}; + const stateChanges = { + currentAccount, + currentSigner, + }; + + // Act: + const wrapper = getAccountDetailsPageWrapper(props, stateChanges); + const component = wrapper.vm as AccountDetailsPageTs; + await component.$nextTick(); + const isComponentShown = component.hasAccountMultisigGraph; + + // Assert: + expect(isComponentShown).toBe(shouldComponentBeShown); + }; + + test('show component when current account is multisig', async () => { + // Arrange: + const currentAccount = { + ...WalletsModel1, + isMultisig: true, + }; + const currentSigner = defaultSigner; + const shouldComponentBeShown = true; + + // Act + Assert: + await runAccountMultisigGraphTest(currentAccount, currentSigner, shouldComponentBeShown); + }); + + test('show component when current signer is multisig cosigner', async () => { + // Arrange: + const currentAccount = WalletsModel1; + const currentSigner = { + ...defaultSigner, + parentSigners: [defaultSigner], + }; + const shouldComponentBeShown = true; + + // Act + Assert: + await runAccountMultisigGraphTest(currentAccount, currentSigner, shouldComponentBeShown); + }); + + test('hide component when current signer is not multisig cosigner ', async () => { + // Arrange: + const currentAccount = WalletsModel1; + const currentSigner = defaultSigner; + const shouldComponentBeShown = false; + + // Act + Assert: + await runAccountMultisigGraphTest(currentAccount, currentSigner, shouldComponentBeShown); + }); + }); +}); diff --git a/package-lock.json b/package-lock.json index d5940ed31..751d21d41 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@fortawesome/vue-fontawesome": "^2.0.2", "@ledgerhq/hw-transport-node-hid-noevents": "^5.41.0", "@ledgerhq/hw-transport-webusb": "^5.41.0", + "@ssthouse/vue-tree-chart": "^0.6.8", "animate.css": "^3.7.2", "await-lock": "^2.0.1", "bip32-path": "^0.4.2", @@ -3095,6 +3096,29 @@ "integrity": "sha512-T7VNNlYVM1SgQ+VsMYhnDkcGmWhQdL0bDyGm5TlQ3GBXnJscEClUUOKduWTmm2zCnvNLC1hc3JpuXjs/nFOc5w==", "dev": true }, + "node_modules/@ssthouse/tree-chart-core": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ssthouse/tree-chart-core/-/tree-chart-core-1.1.2.tgz", + "integrity": "sha512-UiPOu+K8XoS1dPPerhlpf2WZMsREBYnuTWKYcWps/oQPyLdLKbS6ZFBZXaFqm4Q91GEvf+AfD8MK8Cllw2V4kg==", + "dependencies": { + "d3": "^7.2.0" + } + }, + "node_modules/@ssthouse/vue-tree-chart": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@ssthouse/vue-tree-chart/-/vue-tree-chart-0.6.8.tgz", + "integrity": "sha512-7LzPNAcS9kKIQuAS/KADpnfhyRx8WwMOSIDrNVwRoLPAh7U+JqksyK0HtBBkM6356P7oPixDHGsN7mWDkIdcYQ==", + "dependencies": { + "@ssthouse/tree-chart-core": "1.1.2" + }, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + }, + "peerDependencies": { + "vue": "^2.6.8" + } + }, "node_modules/@szmarczak/http-timer": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", @@ -11012,6 +11036,384 @@ "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=" }, + "node_modules/d3": { + "version": "7.6.1", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.6.1.tgz", + "integrity": "sha512-txMTdIHFbcpLx+8a0IFhZsbp+PfBBPt8yfbmukZTQFroKuFqIwqswF0qE5JXWefylaAVpSXFoKm3yP+jpNLFLw==", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.0.tgz", + "integrity": "sha512-3yXFQo0oG3QCxbF06rMPFyGRMGJNS7NvsV1+2joOjbBE+9xvWQ8+GcMJAjRCzw06zQ3/arXeJgbPYcjUCuC+3g==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.0.tgz", + "integrity": "sha512-7aQo0QHUTu/Ko3cP9YK9yUTxtoDEiDGwnBHyLxG5M4vqlBkO/uixMRele3nfsfj6UXOcuReVpVXzAboGraYIJw==", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.2.tgz", + "integrity": "sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.0.1.tgz", + "integrity": "sha512-Wt23xBych5tSy9IYAM1FR2rWIBFWa52B/oF/GYe5zbdHrg08FU8+BuI6X4PvTwPDdqdAdq04fuWJpELtsaEjeA==", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.0.1.tgz", + "integrity": "sha512-gq6gZom9AFZby0YLduxT1qmrp4xpBA1YZr19OI717WIdKE2OM5ETq5qrHLb301IgxhLwcuxvGZVLeeWc/k1I6w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz", + "integrity": "sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.1.0.tgz", + "integrity": "sha512-tGDh1Muf8kWjEDT/LswZJ8WF85yDZLvVJpYU9Nq+8+yW1Z5enxrmXOhTArlkaElU+CTn0OTVNli+/i+HP45QEQ==", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.0.0.tgz", + "integrity": "sha512-zmV3lRnlaLI08y9IMRXSDshQb5Nj77smnfpnd2LrBa/2K281Jijactokeak14QacHs/kKq0AQ121nidNYlarbQ==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -11410,6 +11812,14 @@ "rimraf": "bin.js" } }, + "node_modules/delaunator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.0.tgz", + "integrity": "sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==", + "dependencies": { + "robust-predicates": "^3.0.0" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -16429,7 +16839,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -16777,6 +17186,14 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -27254,6 +27671,11 @@ "dev": true, "optional": true }, + "node_modules/robust-predicates": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.1.tgz", + "integrity": "sha512-ndEIpszUHiG4HtDsQLeIuMvRsDnn8c8rYStabochtUeCvfuvNptb5TUbVD68LRAILPX7p9nqQGh4xJgn3EHS/g==" + }, "node_modules/rss-parser": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/rss-parser/-/rss-parser-3.12.0.tgz", @@ -27301,6 +27723,11 @@ "aproba": "^1.1.1" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" + }, "node_modules/rxjs": { "version": "6.6.7", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", @@ -37047,6 +37474,22 @@ "integrity": "sha512-T7VNNlYVM1SgQ+VsMYhnDkcGmWhQdL0bDyGm5TlQ3GBXnJscEClUUOKduWTmm2zCnvNLC1hc3JpuXjs/nFOc5w==", "dev": true }, + "@ssthouse/tree-chart-core": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ssthouse/tree-chart-core/-/tree-chart-core-1.1.2.tgz", + "integrity": "sha512-UiPOu+K8XoS1dPPerhlpf2WZMsREBYnuTWKYcWps/oQPyLdLKbS6ZFBZXaFqm4Q91GEvf+AfD8MK8Cllw2V4kg==", + "requires": { + "d3": "^7.2.0" + } + }, + "@ssthouse/vue-tree-chart": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@ssthouse/vue-tree-chart/-/vue-tree-chart-0.6.8.tgz", + "integrity": "sha512-7LzPNAcS9kKIQuAS/KADpnfhyRx8WwMOSIDrNVwRoLPAh7U+JqksyK0HtBBkM6356P7oPixDHGsN7mWDkIdcYQ==", + "requires": { + "@ssthouse/tree-chart-core": "1.1.2" + } + }, "@szmarczak/http-timer": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", @@ -43621,6 +44064,276 @@ "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=" }, + "d3": { + "version": "7.6.1", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.6.1.tgz", + "integrity": "sha512-txMTdIHFbcpLx+8a0IFhZsbp+PfBBPt8yfbmukZTQFroKuFqIwqswF0qE5JXWefylaAVpSXFoKm3yP+jpNLFLw==", + "requires": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + } + }, + "d3-array": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.0.tgz", + "integrity": "sha512-3yXFQo0oG3QCxbF06rMPFyGRMGJNS7NvsV1+2joOjbBE+9xvWQ8+GcMJAjRCzw06zQ3/arXeJgbPYcjUCuC+3g==", + "requires": { + "internmap": "1 - 2" + } + }, + "d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==" + }, + "d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "requires": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + } + }, + "d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "requires": { + "d3-path": "1 - 3" + } + }, + "d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==" + }, + "d3-contour": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.0.tgz", + "integrity": "sha512-7aQo0QHUTu/Ko3cP9YK9yUTxtoDEiDGwnBHyLxG5M4vqlBkO/uixMRele3nfsfj6UXOcuReVpVXzAboGraYIJw==", + "requires": { + "d3-array": "^3.2.0" + } + }, + "d3-delaunay": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.2.tgz", + "integrity": "sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==", + "requires": { + "delaunator": "5" + } + }, + "d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==" + }, + "d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "requires": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + } + }, + "d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "requires": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "dependencies": { + "commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" + } + } + }, + "d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==" + }, + "d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "requires": { + "d3-dsv": "1 - 3" + } + }, + "d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "requires": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + } + }, + "d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==" + }, + "d3-geo": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.0.1.tgz", + "integrity": "sha512-Wt23xBych5tSy9IYAM1FR2rWIBFWa52B/oF/GYe5zbdHrg08FU8+BuI6X4PvTwPDdqdAdq04fuWJpELtsaEjeA==", + "requires": { + "d3-array": "2.5.0 - 3" + } + }, + "d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==" + }, + "d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "requires": { + "d3-color": "1 - 3" + } + }, + "d3-path": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.0.1.tgz", + "integrity": "sha512-gq6gZom9AFZby0YLduxT1qmrp4xpBA1YZr19OI717WIdKE2OM5ETq5qrHLb301IgxhLwcuxvGZVLeeWc/k1I6w==" + }, + "d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==" + }, + "d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==" + }, + "d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==" + }, + "d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "requires": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + } + }, + "d3-scale-chromatic": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz", + "integrity": "sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==", + "requires": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + } + }, + "d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==" + }, + "d3-shape": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.1.0.tgz", + "integrity": "sha512-tGDh1Muf8kWjEDT/LswZJ8WF85yDZLvVJpYU9Nq+8+yW1Z5enxrmXOhTArlkaElU+CTn0OTVNli+/i+HP45QEQ==", + "requires": { + "d3-path": "1 - 3" + } + }, + "d3-time": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.0.0.tgz", + "integrity": "sha512-zmV3lRnlaLI08y9IMRXSDshQb5Nj77smnfpnd2LrBa/2K281Jijactokeak14QacHs/kKq0AQ121nidNYlarbQ==", + "requires": { + "d3-array": "2 - 3" + } + }, + "d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "requires": { + "d3-time": "1 - 3" + } + }, + "d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==" + }, + "d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "requires": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + } + }, + "d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "requires": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + } + }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -43925,6 +44638,14 @@ } } }, + "delaunator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.0.tgz", + "integrity": "sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==", + "requires": { + "robust-predicates": "^3.0.0" + } + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -47854,7 +48575,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, "requires": { "safer-buffer": ">= 2.1.2 < 3.0.0" } @@ -48115,6 +48835,11 @@ "side-channel": "^1.0.4" } }, + "internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==" + }, "invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -56434,6 +57159,11 @@ } } }, + "robust-predicates": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.1.tgz", + "integrity": "sha512-ndEIpszUHiG4HtDsQLeIuMvRsDnn8c8rYStabochtUeCvfuvNptb5TUbVD68LRAILPX7p9nqQGh4xJgn3EHS/g==" + }, "rss-parser": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/rss-parser/-/rss-parser-3.12.0.tgz", @@ -56471,6 +57201,11 @@ "aproba": "^1.1.1" } }, + "rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" + }, "rxjs": { "version": "6.6.7", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", diff --git a/package.json b/package.json index 3f26ee4ea..0c9d9e97f 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "@fortawesome/vue-fontawesome": "^2.0.2", "@ledgerhq/hw-transport-node-hid-noevents": "^5.41.0", "@ledgerhq/hw-transport-webusb": "^5.41.0", + "@ssthouse/vue-tree-chart": "^0.6.8", "animate.css": "^3.7.2", "await-lock": "^2.0.1", "bip32-path": "^0.4.2", diff --git a/src/components/AccountMultisigGraph/AccountMultisigGraph.vue b/src/components/AccountMultisigGraph/AccountMultisigGraph.vue index 8b2f2048a..0bf98572d 100644 --- a/src/components/AccountMultisigGraph/AccountMultisigGraph.vue +++ b/src/components/AccountMultisigGraph/AccountMultisigGraph.vue @@ -2,8 +2,38 @@ @@ -24,8 +54,54 @@ export default class AccountLinks extends AccountMultisigGraphTs {} font-size: @normalFont; color: @purpleDark; white-space: pre-line; - line-height: 0.6; - padding: 6px 0; } } + +.show-button { + border: none !important; + font-weight: bold; + cursor: pointer; + background-color: transparent; + color: @purpleLightest; +} + +.multisig-tree { + min-height: 50vh; + border: 1px solid gray; +} + +.multisig-tree-node { + max-width: 3rem; + padding: 0.1rem; + font-family: @symbolFont; + font-size: @normalFont; + line-height: 125%; + color: @purpleDark; + white-space: pre-line; + border-radius: @borderRadius; + border: 0.01rem solid @accentBlue; + background-image: linear-gradient(90deg, @whiteDark, @whiteLight); + user-select: none; + cursor: pointer; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} + +.multisig-tree-node--selected { + color: @white; + border-color: @whiteLight; + background-image: linear-gradient(90deg, @purpleLight, @accentBlue); +} + +.multisig-tree-node--ms { + border-color: @accentPink; +} + +.multisig-tree-node-name { + font-family: @symbolFontBold; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} diff --git a/src/components/AccountMultisigGraph/AccountMultisigGraphTs.ts b/src/components/AccountMultisigGraph/AccountMultisigGraphTs.ts index 3935901a1..71943c561 100644 --- a/src/components/AccountMultisigGraph/AccountMultisigGraphTs.ts +++ b/src/components/AccountMultisigGraph/AccountMultisigGraphTs.ts @@ -14,17 +14,37 @@ * */ import { Component, Prop, Vue } from 'vue-property-decorator'; -// internal dependencies +import VueTree from '@ssthouse/vue-tree-chart'; import { AccountModel } from '@/core/database/entities/AccountModel'; import { mapGetters } from 'vuex'; +import { AddressBook } from 'symbol-address-book'; import { MultisigAccountInfo } from 'symbol-sdk'; import i18n from '@/language'; +interface TreeNode { + name: string; + title: string; + selected: boolean; + children: TreeNode[]; + multisig?: { + minApproval: number; + minRemoval: number; + }; +} + +interface MultisigAccountInfoWithChildren extends MultisigAccountInfo { + children: TreeNode[]; +} + @Component({ + components: { + VueTree, + }, computed: { ...mapGetters({ - multisigAccountGraphInfo: 'account/multisigAccountGraph', + multisigAccountGraphInfo: 'account/multisigAccountGraphInfo', knownAccounts: 'account/knownAccounts', + addressBook: 'addressBook/getAddressBook', }), }, }) @@ -34,73 +54,127 @@ export class AccountMultisigGraphTs extends Vue { }) account: AccountModel; - public multisigAccountGraphInfo: Map; + public multisigAccountGraphInfo: MultisigAccountInfo[]; public knownAccounts: AccountModel[]; + public addressBook: AddressBook; + public graphConfig = {}; + public isGraphModalShown = false; - get multisigGraphTree(): any[] { - if (this.multisigAccountGraphInfo?.size) { - return this.getMultisigDisplayGraph(this.multisigAccountGraphInfo); + get dataset(): TreeNode[] { + if (!this.multisigAccountGraphInfo?.length) { + return []; } - return []; + + const tree: TreeNode[] = []; + const hashTable = {}; + const sortedMultisigAccountGraphInfo = this.multisigAccountGraphInfo.sort((a, b) => + a.accountAddress.plain() > b.accountAddress.plain() ? 1 : b.accountAddress.plain() > a.accountAddress.plain() ? -1 : 0, + ); + sortedMultisigAccountGraphInfo.forEach((multisigAccountInfo) => { + const address = multisigAccountInfo.accountAddress.plain(); + hashTable[address] = { + ...multisigAccountInfo, + children: [], + }; + }); + sortedMultisigAccountGraphInfo.forEach((multisigAccountInfo) => { + const address = multisigAccountInfo.accountAddress.plain(); + + if (multisigAccountInfo.multisigAddresses.length) { + const parentPlainAddresses = multisigAccountInfo.multisigAddresses.map((address) => address.plain()); + parentPlainAddresses.forEach((parentAddress) => { + const treeNode = this.multisigAccountInfoToTreeNode(hashTable[address]); + hashTable[parentAddress]?.children.push(treeNode); + }); + } else { + const treeNode = this.multisigAccountInfoToTreeNode(hashTable[address]); + tree.push(treeNode); + } + }); + + return tree; } - public getMultisigDisplayGraph(multisigEntries: Map): any[] { - const firstLevelNumber = [...multisigEntries.keys()].sort()[0]; - const firstLevel = multisigEntries.get(firstLevelNumber); - const graph = []; - for (const entry of firstLevel) { - graph.push({ - info: entry, - address: entry.accountAddress.plain(), - title: this.getAccountLabel(entry, this.knownAccounts), - children: [this.getMultisigDisplayGraphChildren(firstLevelNumber + 1, entry, multisigEntries)], - selected: this.account.address === entry.accountAddress.plain(), - }); + private multisigAccountInfoToTreeNode(info: MultisigAccountInfoWithChildren): TreeNode { + const address = info.accountAddress.plain(); + const name = this.getAccountLabel(address, true); + const selected = this.account.address === address; + const children = info.children; + const cosignatoryAddressesCount = info.cosignatoryAddresses.length; + let multisig = null; + let title = this.getAccountLabel(address, false); + + if (cosignatoryAddressesCount > 0) { + multisig = { + minApproval: `${i18n.t('form_label_min_approval')}: ${info.minApproval}`, + minRemoval: `${i18n.t('form_label_min_removal')}: ${info.minRemoval}`, + }; + title += ` + ${info.minApproval} of ${info.cosignatoryAddresses.length} ${i18n.t('label_for_approvals')} + ${info.minRemoval} of ${info.cosignatoryAddresses.length} ${i18n.t('label_for_removals')}`; } - return graph[0].children; + + return { + name, + title, + selected, + children, + multisig, + }; } - private getMultisigDisplayGraphChildren( - level: number, - info: MultisigAccountInfo, - multisigEntries: Map, - ): any { - const entries = multisigEntries.get(level); - if (!entries) { - return { - info: info, - address: info.accountAddress.plain(), - title: this.getAccountLabel(info, this.knownAccounts), - children: [], - selected: this.account.address === info.accountAddress.plain(), - }; + private getAccountLabel(address: string, shortenAddress: boolean): string { + const contact = this.addressBook.getContactByAddress(address); + + if (contact) { + return contact.name; } - const children = []; - for (const entry of entries) { - const isFromParent = entry.multisigAddresses.find((address) => address.plain() === info.accountAddress.plain()); - if (isFromParent) { - children.push(this.getMultisigDisplayGraphChildren(level + 1, entry, multisigEntries)); - } + + const account = this.knownAccounts.find((knownAccount) => address === knownAccount.address); + + if (account) { + return account.name; } - return { - info: info, - address: info.accountAddress.plain(), - title: this.getAccountLabel(info, this.knownAccounts), - children: children, - selected: this.account.address === info.accountAddress.plain(), + + if (shortenAddress) { + const addressStart = address.substring(0, 5); + const addressEnd = address.slice(-5); + + return `${addressStart}...${addressEnd}`; + } + + return address; + } + + public showGraphModal() { + this.isGraphModalShown = true; + this.updateGraphConfig(); + this.centerGraph(); + } + + private centerGraph() { + const offsetX = this.remToPixels(5.8); + const stylePositionCeneter = `transform: scale(1) translate(${offsetX}px, 0px); transform-origin: center center;`; + const vueTree = this.$refs['VueTree'] as Vue; + vueTree.$el.children[0].setAttribute('style', stylePositionCeneter); + vueTree.$el.children[1].setAttribute('style', stylePositionCeneter); + } + + private updateGraphConfig() { + this.graphConfig = { + nodeWidth: this.remToPixels(2), + nodeHeight: this.remToPixels(1), + levelHeight: this.remToPixels(1), + linkStyle: 'streight', + collapseEnabled: false, }; } - getAccountLabel(info: MultisigAccountInfo, accounts: AccountModel[]): string { - const account = accounts.find((wlt) => info.accountAddress.plain() === wlt.address); - const addressOrName = (account && account.name) || info.accountAddress.plain(); - return ( - addressOrName + - (info.isMultisig() - ? `\n - ${info.minApproval} of ${info.cosignatoryAddresses.length} ${i18n.t('label_for_approvals')} \n - ${info.minRemoval} of ${info.cosignatoryAddresses.length} ${i18n.t('label_for_removals')}` - : '') - ); + private remToPixels(rem: number): number { + return Math.round(rem * parseFloat(getComputedStyle(document.documentElement).fontSize)); + } + + created() { + this.updateGraphConfig(); } } diff --git a/src/store/Account.ts b/src/store/Account.ts index 7b56c0675..dd8a11fc9 100644 --- a/src/store/Account.ts +++ b/src/store/Account.ts @@ -578,17 +578,34 @@ export default { .getMultisigAccountGraphInfo(currentAccountAddress) .toPromise(); - const currentMultisigAccountsInfo = MultisigService.getMultisigInfoFromMultisigGraphInfo(currentMultisigAccountGraphInfo); - const rootAddress = currentMultisigAccountsInfo.pop().accountAddress; - - const rootMultisigAccountGraphInfo = await repositoryFactory - .createMultisigRepository() - .getMultisigAccountGraphInfo(rootAddress) - .toPromise(); + const rootMultisigAccountGraph = new Map(currentMultisigAccountGraphInfo.multisigEntries); + + for (const [currentLevel, multisigAccountInfos] of currentMultisigAccountGraphInfo.multisigEntries) { + if (currentLevel < 0) { + for (const multisigAccountInfo of multisigAccountInfos) { + const fetchedMultisigAccountGraphInfo = await repositoryFactory + .createMultisigRepository() + .getMultisigAccountGraphInfo(multisigAccountInfo.accountAddress) + .toPromise(); + + fetchedMultisigAccountGraphInfo.multisigEntries.forEach((fetchedMultisigAccountInfos, fetchedLevel) => { + const currentMultisigAccountInfos = rootMultisigAccountGraph.get(currentLevel + fetchedLevel) || []; + const newMultisigAccountInfos = [...currentMultisigAccountInfos, ...fetchedMultisigAccountInfos]; + const filteredMultisigAccountInfos = _.uniqBy(newMultisigAccountInfos, (item) => + item.accountAddress.plain(), + ); + + rootMultisigAccountGraph.set(currentLevel + fetchedLevel, filteredMultisigAccountInfos); + }); + } + } + } - const rootMultisigAccountsInfo = MultisigService.getMultisigInfoFromMultisigGraphInfo(rootMultisigAccountGraphInfo); + const rootMultisigAccountsInfo = MultisigService.getMultisigInfoFromMultisigGraphInfo({ + multisigEntries: rootMultisigAccountGraph, + }); - commit('multisigAccountGraph', rootMultisigAccountGraphInfo.multisigEntries); + commit('multisigAccountGraph', rootMultisigAccountGraph); commit('multisigAccountGraphInfo', rootMultisigAccountsInfo); return rootMultisigAccountsInfo; diff --git a/src/views/pages/accounts/AccountDetailsPage/AccountDetailsPage.vue b/src/views/pages/accounts/AccountDetailsPage/AccountDetailsPage.vue index 818896d3e..70454140a 100644 --- a/src/views/pages/accounts/AccountDetailsPage/AccountDetailsPage.vue +++ b/src/views/pages/accounts/AccountDetailsPage/AccountDetailsPage.vue @@ -63,8 +63,7 @@
- - +