From 90fbadefffc1971db41e473581a173bbcc26965f Mon Sep 17 00:00:00 2001 From: vferraro-scottlogic Date: Mon, 16 Oct 2023 09:50:04 +0100 Subject: [PATCH 01/56] VUU-27 remotePersistenceManager implementation --- .../LayoutPersistenceManager.ts | 2 +- .../LocalLayoutPersistenceManager.ts | 107 +++--- .../RemoteLayoutPersistenceManager.ts | 137 ++++++++ .../src/layout-persistence/index.ts | 1 + .../LocalLayoutPersistenceManager.test.ts | 270 +++++++++------ .../RemoteLayoutPersistenceManager.test.ts | 311 ++++++++++++++++++ .../test/layout-persistence/utils.ts | 8 + .../src/layout-management/SaveLayoutPanel.tsx | 7 +- .../src/layout-management/layoutTypes.ts | 2 +- .../layout-management/useLayoutManager.tsx | 31 +- .../src/examples/Apps/NewTheme.examples.tsx | 2 +- vuu-ui/showcase/vite.config.js | 1 + 12 files changed, 724 insertions(+), 155 deletions(-) create mode 100644 vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts create mode 100644 vuu-ui/packages/vuu-layout/test/layout-persistence/RemoteLayoutPersistenceManager.test.ts create mode 100644 vuu-ui/packages/vuu-layout/test/layout-persistence/utils.ts diff --git a/vuu-ui/packages/vuu-layout/src/layout-persistence/LayoutPersistenceManager.ts b/vuu-ui/packages/vuu-layout/src/layout-persistence/LayoutPersistenceManager.ts index 54fbf9d54..39d8be719 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-persistence/LayoutPersistenceManager.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-persistence/LayoutPersistenceManager.ts @@ -10,7 +10,7 @@ export interface LayoutPersistenceManager { * * @returns Unique identifier assigned to the saved layout */ - createLayout: (metadata: Omit, layout: LayoutJSON) => Promise; + createLayout: (metadata: Omit, layout: LayoutJSON) => Promise; /** * Overwrites an existing layout and its corresponding metadata with the provided information diff --git a/vuu-ui/packages/vuu-layout/src/layout-persistence/LocalLayoutPersistenceManager.ts b/vuu-ui/packages/vuu-layout/src/layout-persistence/LocalLayoutPersistenceManager.ts index 4a86b464b..2d1f9603b 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-persistence/LocalLayoutPersistenceManager.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-persistence/LocalLayoutPersistenceManager.ts @@ -1,31 +1,43 @@ import { Layout, LayoutMetadata, WithId } from "@finos/vuu-shell"; -import { LayoutJSON, LayoutPersistenceManager } from "@finos/vuu-layout"; import { getLocalEntity, saveLocalEntity } from "@finos/vuu-filters"; -import { getUniqueId } from "@finos/vuu-utils"; +import { formatDate, getUniqueId } from "@finos/vuu-utils"; import { defaultLayout } from "./data"; +import { LayoutPersistenceManager } from "./LayoutPersistenceManager"; +import { LayoutJSON } from "../layout-reducer"; const metadataSaveLocation = "layouts/metadata"; const layoutsSaveLocation = "layouts/layouts"; export class LocalLayoutPersistenceManager implements LayoutPersistenceManager { - createLayout(metadata: Omit, layout: LayoutJSON): Promise { - return new Promise(resolve => { - console.log(`Saving layout as ${metadata.name} to group ${metadata.group}...`); + createLayout( + metadata: Omit, + layout: LayoutJSON + ): Promise { + return new Promise((resolve) => { + console.log( + `Saving layout as ${metadata.name} to group ${metadata.group}...` + ); - Promise.all([this.loadLayouts(), this.loadMetadata()]) - .then(([existingLayouts, existingMetadata]) => { + Promise.all([this.loadLayouts(), this.loadMetadata()]).then( + ([existingLayouts, existingMetadata]) => { const id = getUniqueId(); + const newMetadata = { + ...metadata, + id, + created: formatDate(new Date(), "dd.mm.yyyy"), + }; this.appendAndPersist( id, - metadata, + newMetadata, layout, existingLayouts, existingMetadata ); - resolve(id); - }); - }) + resolve(newMetadata); + } + ); + }); } updateLayout( @@ -37,12 +49,14 @@ export class LocalLayoutPersistenceManager implements LayoutPersistenceManager { this.validateIds(id) .then(() => Promise.all([this.loadLayouts(), this.loadMetadata()])) .then(([existingLayouts, existingMetadata]) => { - const layouts = existingLayouts.filter(layout => layout.id !== id); - const metadata = existingMetadata.filter(metadata => metadata.id !== id); + const layouts = existingLayouts.filter((layout) => layout.id !== id); + const metadata = existingMetadata.filter( + (metadata) => metadata.id !== id + ); this.appendAndPersist(id, newMetadata, newLayout, layouts, metadata); resolve(); }) - .catch(e => reject(e)); + .catch((e) => reject(e)); }); } @@ -51,12 +65,14 @@ export class LocalLayoutPersistenceManager implements LayoutPersistenceManager { this.validateIds(id) .then(() => Promise.all([this.loadLayouts(), this.loadMetadata()])) .then(([existingLayouts, existingMetadata]) => { - const layouts = existingLayouts.filter(layout => layout.id !== id); - const metadata = existingMetadata.filter(metadata => metadata.id !== id); + const layouts = existingLayouts.filter((layout) => layout.id !== id); + const metadata = existingMetadata.filter( + (metadata) => metadata.id !== id + ); this.saveLayoutsWithMetadata(layouts, metadata); resolve(); }) - .catch(e => reject(e)); + .catch((e) => reject(e)); }); } @@ -64,11 +80,17 @@ export class LocalLayoutPersistenceManager implements LayoutPersistenceManager { return new Promise((resolve, reject) => { this.validateId(id, "layout") .then(() => this.loadLayouts()) - .then(existingLayouts => { - const layouts = existingLayouts.find(layout => layout.id === id) as Layout; - resolve(layouts.json); + .then((existingLayouts) => { + const foundLayout = existingLayouts.find( + (layout) => layout.id === id + ); + if (foundLayout) { + resolve(foundLayout.json); + } else { + reject(new Error(`no layout found matching id ${id}`)); + } }) - .catch(e => reject(e)); + .catch((e) => reject(e)); }); } @@ -102,7 +124,7 @@ export class LocalLayoutPersistenceManager implements LayoutPersistenceManager { } private loadLayouts(): Promise { - return new Promise(resolve => { + return new Promise((resolve) => { const layouts = getLocalEntity(layoutsSaveLocation); resolve(layouts || []); }); @@ -132,29 +154,33 @@ export class LocalLayoutPersistenceManager implements LayoutPersistenceManager { // Ensures that there is exactly one Layout entry and exactly one Metadata // entry in local storage corresponding to the provided ID. private async validateIds(id: string): Promise { - return Promise - .all([ - this.validateId(id, "metadata").catch(error => error.message), - this.validateId(id, "layout").catch(error => error.message) - ]) - .then((errorMessages: string[]) => { - // filter() is used to remove any blank messages before joining. - // Avoids orphaned delimiters in combined messages, e.g. "; " or "; error 2" - const combinedMessage = errorMessages.filter(msg => msg !== undefined).join("; "); - if (combinedMessage) { - throw new Error(combinedMessage); - } - }); + return Promise.all([ + this.validateId(id, "metadata").catch((error) => error.message), + this.validateId(id, "layout").catch((error) => error.message), + ]).then((errorMessages: string[]) => { + // filter() is used to remove any blank messages before joining. + // Avoids orphaned delimiters in combined messages, e.g. "; " or "; error 2" + const combinedMessage = errorMessages + .filter((msg) => msg !== undefined) + .join("; "); + if (combinedMessage) { + throw new Error(combinedMessage); + } + }); } // Ensures that there is exactly one element (Layout or Metadata) in local // storage corresponding to the provided ID. - private validateId(id: string, dataType: "metadata" | "layout"): Promise { + private validateId( + id: string, + dataType: "metadata" | "layout" + ): Promise { return new Promise((resolve, reject) => { - const loadFunc = dataType === "metadata" ? this.loadMetadata : this.loadLayouts; + const loadFunc = + dataType === "metadata" ? this.loadMetadata : this.loadLayouts; loadFunc().then((array: WithId[]) => { - const count = array.filter(element => element.id === id).length; + const count = array.filter((element) => element.id === id).length; switch (count) { case 1: { resolve(); @@ -164,9 +190,10 @@ export class LocalLayoutPersistenceManager implements LayoutPersistenceManager { reject(new Error(`No ${dataType} with ID ${id}`)); break; } - default: reject(new Error(`Non-unique ${dataType} with ID ${id}`)); + default: + reject(new Error(`Non-unique ${dataType} with ID ${id}`)); } }); - }) + }); } } diff --git a/vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts b/vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts new file mode 100644 index 000000000..ca63c8689 --- /dev/null +++ b/vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts @@ -0,0 +1,137 @@ +import { LayoutMetadata } from "@finos/vuu-shell"; +import { LayoutPersistenceManager } from "./LayoutPersistenceManager"; +import { LayoutJSON } from "../layout-reducer"; +import { defaultLayout } from "./data"; + +const baseURL = "http://127.0.0.1:8081/api"; +const metadataSaveLocation = "layouts/metadata"; +const layoutsSaveLocation = "layouts"; + +export class RemoteLayoutPersistenceManager + implements LayoutPersistenceManager +{ + createLayout( + metadata: Omit, + layout: LayoutJSON + ): Promise { + return new Promise((resolve, reject) => + fetch(`${baseURL}/${layoutsSaveLocation}`, { + headers: { + "Content-Type": "application/json", + }, + method: "POST", + body: JSON.stringify({ + metadata: { ...metadata }, + definition: JSON.stringify(layout), + }), + }) + .then((response) => { + if (!response.ok) { + reject(new Error(response.statusText)); + } + response.json().then(({ metadata }: { metadata: LayoutMetadata }) => { + if (!metadata) { + reject(new Error("invalid metadata")); + } + resolve(metadata); + }); + }) + .catch((error: Error) => { + reject(error); + }) + ); + } + + updateLayout( + id: string, + metadata: Omit, + newLayoutJson: LayoutJSON + ): Promise { + return new Promise((resolve, reject) => + fetch(`${baseURL}/${layoutsSaveLocation}/${id}`, { + method: "PUT", + body: JSON.stringify({ + metadata, + layout: newLayoutJson, + }), + }) + .then((response) => { + if (!response.ok) { + reject(new Error(response.statusText)); + } + resolve(); + }) + .catch((error: Error) => { + reject(error); + }) + ); + } + + deleteLayout(id: string): Promise { + return new Promise((resolve, reject) => + fetch(`${baseURL}/${layoutsSaveLocation}/${id}`, { + method: "DELETE", + }) + .then((response) => { + if (!response.ok) { + reject(new Error(response.statusText)); + } + resolve(); + }) + .catch((error: Error) => { + reject(error); + }) + ); + } + + loadLayout(id: string): Promise { + return new Promise((resolve, reject) => { + fetch(`${baseURL}/${layoutsSaveLocation}/${id}`, {}) + .then((response) => { + if (!response.ok) { + reject(new Error(response.statusText)); + } + response.json().then((layout) => { + if (!layout) { + reject(new Error("invalid layout")); + } + resolve(layout); + }); + }) + .catch((error: Error) => { + reject(error); + }); + }); + } + + loadMetadata(): Promise { + return new Promise((resolve, reject) => + fetch(`${baseURL}/${metadataSaveLocation}`, {}) + .then(async (response) => { + if (!response.ok) { + reject(new Error(response.statusText)); + } + response.json().then((metadata: LayoutMetadata[]) => { + if (!metadata) { + reject(new Error("invalid metadata")); + } + resolve(metadata); + }); + }) + .catch((error: Error) => { + reject(error); + }) + ); + } + + saveApplicationLayout(layout: LayoutJSON): Promise { + // TODO POST api/layouts/application #71 + console.log(layout); + return new Promise((resolve) => resolve()); + } + + loadApplicationLayout(): Promise { + // TODO GET api/layouts/application #71 + return new Promise((resolve) => resolve(defaultLayout)); + } +} diff --git a/vuu-ui/packages/vuu-layout/src/layout-persistence/index.ts b/vuu-ui/packages/vuu-layout/src/layout-persistence/index.ts index a047506db..8a0925e1f 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-persistence/index.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-persistence/index.ts @@ -1,3 +1,4 @@ export * from './LayoutPersistenceManager'; export * from './LocalLayoutPersistenceManager'; +export * from './RemoteLayoutPersistenceManager'; export * from './data'; \ No newline at end of file diff --git a/vuu-ui/packages/vuu-layout/test/layout-persistence/LocalLayoutPersistenceManager.test.ts b/vuu-ui/packages/vuu-layout/test/layout-persistence/LocalLayoutPersistenceManager.test.ts index 1b08d6072..41a58644c 100644 --- a/vuu-ui/packages/vuu-layout/test/layout-persistence/LocalLayoutPersistenceManager.test.ts +++ b/vuu-ui/packages/vuu-layout/test/layout-persistence/LocalLayoutPersistenceManager.test.ts @@ -2,7 +2,9 @@ import { Layout, LayoutMetadata } from "@finos/vuu-shell"; import { afterEach, describe, expect, it, vi } from "vitest"; import { LocalLayoutPersistenceManager } from "../../src/layout-persistence"; import { LayoutJSON } from "../../src/layout-reducer"; -import { getLocalEntity, saveLocalEntity } from "../../../vuu-filters/src/local-config"; +import { getLocalEntity, saveLocalEntity } from "@finos/vuu-filters"; +import { formatDate } from "@finos/vuu-utils"; +import { expectPromiseRejectsWithError } from "./utils"; vi.mock("@finos/vuu-filters", async () => { return { @@ -25,26 +27,35 @@ const persistenceManager = new LocalLayoutPersistenceManager(); const existingId = "existing_id"; +const newDate = formatDate(new Date(), "dd.mm.yyyy"); + const existingMetadata: LayoutMetadata = { id: existingId, name: "Existing Layout", group: "Group 1", screenshot: "screenshot", user: "vuu user", - date: "01/01/2023", + created: newDate, }; const existingLayout: Layout = { id: existingId, - json: { type: "t0" } + json: { type: "t0" }, }; -const metadataToAdd: Omit = { +const metadataToAdd: Omit = { name: "New Layout", group: "Group 1", screenshot: "screenshot", user: "vuu user", - date: "26/09/2023", +}; + +const metadataToUpdate: Omit = { + name: "New Layout", + group: "Group 1", + screenshot: "screenshot", + user: "vuu user", + created: newDate, }; const layoutToAdd: LayoutJSON = { @@ -59,21 +70,25 @@ afterEach(() => { }) describe("createLayout", () => { - it("persists to local storage with a unique ID", async () => { - const returnedId = await persistenceManager.createLayout(metadataToAdd, layoutToAdd); + const { id, created } = await persistenceManager.createLayout( + metadataToAdd, + layoutToAdd + ); - const persistedMetadata = getLocalEntity(metadataSaveLocation); + const persistedMetadata = + getLocalEntity(metadataSaveLocation); const persistedLayout = getLocalEntity(layoutsSaveLocation); const expectedMetadata: LayoutMetadata = { ...metadataToAdd, - id: returnedId + id, + created, }; const expectedLayout: Layout = { json: layoutToAdd, - id: returnedId + id, }; expect(persistedMetadata).toEqual([expectedMetadata]); @@ -84,20 +99,25 @@ describe("createLayout", () => { saveLocalEntity(metadataSaveLocation, [existingMetadata]); saveLocalEntity(layoutsSaveLocation, [existingLayout]); - const returnedId = await persistenceManager.createLayout(metadataToAdd, layoutToAdd); - expect(returnedId).not.toEqual(existingId); + const { id, created } = await persistenceManager.createLayout( + metadataToAdd, + layoutToAdd + ); + expect(id).not.toEqual(existingId); - const persistedMetadata = getLocalEntity(metadataSaveLocation); + const persistedMetadata = + getLocalEntity(metadataSaveLocation); const persistedLayout = getLocalEntity(layoutsSaveLocation); const expectedMetadata: LayoutMetadata = { ...metadataToAdd, - id: returnedId, + id, + created, }; const expectedLayout: Layout = { json: layoutToAdd, - id: returnedId, + id, }; expect(persistedMetadata).toEqual([existingMetadata, expectedMetadata]); @@ -106,18 +126,22 @@ describe("createLayout", () => { }); describe("updateLayout", () => { - it("updates an existing layout", async () => { saveLocalEntity(metadataSaveLocation, [existingMetadata]); saveLocalEntity(layoutsSaveLocation, [existingLayout]); - await persistenceManager.updateLayout(existingId, metadataToAdd, layoutToAdd); + await persistenceManager.updateLayout( + existingId, + metadataToUpdate, + layoutToAdd + ); - const persistedMetadata = getLocalEntity(metadataSaveLocation); + const persistedMetadata = + getLocalEntity(metadataSaveLocation); const persistedLayout = getLocalEntity(layoutsSaveLocation); const expectedMetadata: LayoutMetadata = { - ...metadataToAdd, + ...metadataToUpdate, id: existingId, }; @@ -133,156 +157,211 @@ describe("updateLayout", () => { it("errors if there is no metadata in local storage with requested ID ", async () => { saveLocalEntity(layoutsSaveLocation, [existingLayout]); - expectError(() => - persistenceManager.updateLayout(existingId, metadataToAdd, layoutToAdd), - `No metadata with ID ${existingId}`); + expectPromiseRejectsWithError( + () => + persistenceManager.updateLayout( + existingId, + metadataToUpdate, + layoutToAdd + ), + `No metadata with ID ${existingId}` + ); }); it("errors if there is no layout in local storage with requested ID ", async () => { saveLocalEntity(metadataSaveLocation, [existingMetadata]); - expectError(() => - persistenceManager.updateLayout(existingId, metadataToAdd, layoutToAdd), - `No layout with ID ${existingId}`); + expectPromiseRejectsWithError( + () => + persistenceManager.updateLayout( + existingId, + metadataToUpdate, + layoutToAdd + ), + `No layout with ID ${existingId}` + ); }); it("errors if there is no metadata or layout in local storage with requested ID ", async () => { const requestedId = "non_existent_id"; - expectError(() => - persistenceManager.updateLayout(requestedId, metadataToAdd, layoutToAdd), - `No metadata with ID ${requestedId}; No layout with ID ${requestedId}`); + expectPromiseRejectsWithError( + () => + persistenceManager.updateLayout( + requestedId, + metadataToUpdate, + layoutToAdd + ), + `No metadata with ID ${requestedId}; No layout with ID ${requestedId}` + ); }); it("errors if there are multiple metadata entries in local storage with requested ID ", async () => { saveLocalEntity(metadataSaveLocation, [existingMetadata, existingMetadata]); saveLocalEntity(layoutsSaveLocation, [existingLayout]); - expectError(() => - persistenceManager.updateLayout(existingId, metadataToAdd, layoutToAdd), - `Non-unique metadata with ID ${existingId}`); + expectPromiseRejectsWithError( + () => + persistenceManager.updateLayout( + existingId, + metadataToUpdate, + layoutToAdd + ), + `Non-unique metadata with ID ${existingId}` + ); }); it("errors if there are multiple layouts in local storage with requested ID ", async () => { saveLocalEntity(metadataSaveLocation, [existingMetadata]); saveLocalEntity(layoutsSaveLocation, [existingLayout, existingLayout]); - expectError(() => - persistenceManager.updateLayout(existingId, metadataToAdd, layoutToAdd), - `Non-unique layout with ID ${existingId}`); + expectPromiseRejectsWithError( + () => + persistenceManager.updateLayout( + existingId, + metadataToUpdate, + layoutToAdd + ), + `Non-unique layout with ID ${existingId}` + ); }); it("errors if there are multiple metadata entries and multiple layouts in local storage with requested ID ", async () => { saveLocalEntity(metadataSaveLocation, [existingMetadata, existingMetadata]); saveLocalEntity(layoutsSaveLocation, [existingLayout, existingLayout]); - expectError(() => - persistenceManager.updateLayout(existingId, metadataToAdd, layoutToAdd), - `Non-unique metadata with ID ${existingId}; Non-unique layout with ID ${existingId}`); + expectPromiseRejectsWithError( + () => + persistenceManager.updateLayout( + existingId, + metadataToUpdate, + layoutToAdd + ), + `Non-unique metadata with ID ${existingId}; Non-unique layout with ID ${existingId}` + ); }); it("errors if there are multiple metadata entries and no layouts in local storage with requested ID ", async () => { saveLocalEntity(metadataSaveLocation, [existingMetadata, existingMetadata]); - expectError(() => - persistenceManager.updateLayout(existingId, metadataToAdd, layoutToAdd), - `Non-unique metadata with ID ${existingId}; No layout with ID ${existingId}`); + expectPromiseRejectsWithError( + () => + persistenceManager.updateLayout( + existingId, + metadataToUpdate, + layoutToAdd + ), + `Non-unique metadata with ID ${existingId}; No layout with ID ${existingId}` + ); }); it("errors if there are no metadata entries and multiple layouts in local storage with requested ID ", async () => { saveLocalEntity(layoutsSaveLocation, [existingLayout, existingLayout]); - expectError(() => - persistenceManager.updateLayout(existingId, metadataToAdd, layoutToAdd), - `No metadata with ID ${existingId}; Non-unique layout with ID ${existingId}`); + expectPromiseRejectsWithError( + () => + persistenceManager.updateLayout( + existingId, + metadataToUpdate, + layoutToAdd + ), + `No metadata with ID ${existingId}; Non-unique layout with ID ${existingId}` + ); }); }); describe("deleteLayout", () => { - it("removes items from storage", async () => { saveLocalEntity(metadataSaveLocation, [existingMetadata]); saveLocalEntity(layoutsSaveLocation, [existingLayout]); await persistenceManager.deleteLayout(existingId); - const persistedMetadata = getLocalEntity(metadataSaveLocation); + const persistedMetadata = + getLocalEntity(metadataSaveLocation); const persistedLayouts = getLocalEntity(layoutsSaveLocation); expect(persistedMetadata).toEqual([]); expect(persistedLayouts).toEqual([]); - }) + }); it("errors if there is no metadata in local storage with requested ID ", async () => { saveLocalEntity(layoutsSaveLocation, [existingLayout]); - expectError(() => - persistenceManager.deleteLayout(existingId), - `No metadata with ID ${existingId}`); + expectPromiseRejectsWithError( + () => persistenceManager.deleteLayout(existingId), + `No metadata with ID ${existingId}` + ); }); it("errors if there is no layout in local storage with requested ID ", async () => { saveLocalEntity(metadataSaveLocation, [existingMetadata]); - expectError(() => - persistenceManager.deleteLayout(existingId), - `No layout with ID ${existingId}`); + expectPromiseRejectsWithError( + () => persistenceManager.deleteLayout(existingId), + `No layout with ID ${existingId}` + ); }); it("errors if there is no metadata or layout in local storage with requested ID ", async () => { const requestedId = "non_existent_id"; - expectError(() => - persistenceManager.deleteLayout(requestedId), - `No metadata with ID ${requestedId}; No layout with ID ${requestedId}`); + expectPromiseRejectsWithError( + () => persistenceManager.deleteLayout(requestedId), + `No metadata with ID ${requestedId}; No layout with ID ${requestedId}` + ); }); it("errors if there are multiple metadata entries in local storage with requested ID ", async () => { saveLocalEntity(metadataSaveLocation, [existingMetadata, existingMetadata]); saveLocalEntity(layoutsSaveLocation, [existingLayout]); - expectError(() => - persistenceManager.deleteLayout(existingId), - `Non-unique metadata with ID ${existingId}`); + expectPromiseRejectsWithError( + () => persistenceManager.deleteLayout(existingId), + `Non-unique metadata with ID ${existingId}` + ); }); it("errors if there are multiple layouts in local storage with requested ID ", async () => { saveLocalEntity(metadataSaveLocation, [existingMetadata]); saveLocalEntity(layoutsSaveLocation, [existingLayout, existingLayout]); - expectError(() => - persistenceManager.deleteLayout(existingId), - `Non-unique layout with ID ${existingId}`); + expectPromiseRejectsWithError( + () => persistenceManager.deleteLayout(existingId), + `Non-unique layout with ID ${existingId}` + ); }); it("errors if there are multiple metadata entries and multiple layouts in local storage with requested ID ", async () => { saveLocalEntity(metadataSaveLocation, [existingMetadata, existingMetadata]); saveLocalEntity(layoutsSaveLocation, [existingLayout, existingLayout]); - expectError(() => - persistenceManager.deleteLayout(existingId), - `Non-unique metadata with ID ${existingId}; Non-unique layout with ID ${existingId}`); + expectPromiseRejectsWithError( + () => persistenceManager.deleteLayout(existingId), + `Non-unique metadata with ID ${existingId}; Non-unique layout with ID ${existingId}` + ); }); it("errors if there are multiple metadata entries and no layouts in local storage with requested ID ", async () => { saveLocalEntity(metadataSaveLocation, [existingMetadata, existingMetadata]); - expectError(() => - persistenceManager.deleteLayout(existingId), - `Non-unique metadata with ID ${existingId}; No layout with ID ${existingId}`); + expectPromiseRejectsWithError( + () => persistenceManager.deleteLayout(existingId), + `Non-unique metadata with ID ${existingId}; No layout with ID ${existingId}` + ); }); it("errors if there are no metadata entries and multiple layouts in local storage with requested ID ", async () => { saveLocalEntity(layoutsSaveLocation, [existingLayout, existingLayout]); - expectError(() => - persistenceManager.deleteLayout(existingId), - `No metadata with ID ${existingId}; Non-unique layout with ID ${existingId}`); + expectPromiseRejectsWithError( + () => persistenceManager.deleteLayout(existingId), + `No metadata with ID ${existingId}; Non-unique layout with ID ${existingId}` + ); }); }); describe("loadLayout", () => { - it("retrieves a persisted layout", async () => { saveLocalEntity(metadataSaveLocation, [existingMetadata]); saveLocalEntity(layoutsSaveLocation, [existingLayout]); @@ -303,17 +382,19 @@ describe("loadLayout", () => { it("errors if there is no layout in local storage with requested ID ", async () => { saveLocalEntity(metadataSaveLocation, [existingMetadata]); - expectError(() => - persistenceManager.loadLayout(existingId), - `No layout with ID ${existingId}`); + expectPromiseRejectsWithError( + () => persistenceManager.loadLayout(existingId), + `No layout with ID ${existingId}` + ); }); it("errors if there is no metadata or layout in local storage with requested ID ", async () => { const requestedId = "non_existent_id"; - expectError(() => - persistenceManager.loadLayout(requestedId), - `No layout with ID ${requestedId}`); + expectPromiseRejectsWithError( + () => persistenceManager.loadLayout(requestedId), + `No layout with ID ${requestedId}` + ); }); it("retrieves layout if there are multiple metadata entries in local storage with requested ID ", async () => { @@ -329,39 +410,42 @@ describe("loadLayout", () => { saveLocalEntity(metadataSaveLocation, [existingMetadata]); saveLocalEntity(layoutsSaveLocation, [existingLayout, existingLayout]); - expectError(() => - persistenceManager.loadLayout(existingId), - `Non-unique layout with ID ${existingId}`); + expectPromiseRejectsWithError( + () => persistenceManager.loadLayout(existingId), + `Non-unique layout with ID ${existingId}` + ); }); it("errors if there are multiple metadata entries and multiple layouts in local storage with requested ID ", async () => { saveLocalEntity(metadataSaveLocation, [existingMetadata, existingMetadata]); saveLocalEntity(layoutsSaveLocation, [existingLayout, existingLayout]); - expectError(() => - persistenceManager.loadLayout(existingId), - `Non-unique layout with ID ${existingId}`); + expectPromiseRejectsWithError( + () => persistenceManager.loadLayout(existingId), + `Non-unique layout with ID ${existingId}` + ); }); it("errors if there are multiple metadata entries and no layouts in local storage with requested ID ", async () => { saveLocalEntity(metadataSaveLocation, [existingMetadata, existingMetadata]); - expectError(() => - persistenceManager.loadLayout(existingId), - `No layout with ID ${existingId}`); + expectPromiseRejectsWithError( + () => persistenceManager.loadLayout(existingId), + `No layout with ID ${existingId}` + ); }); it("errors if there are no metadata entries and multiple layouts in local storage with requested ID ", async () => { saveLocalEntity(layoutsSaveLocation, [existingLayout, existingLayout]); - expectError(() => - persistenceManager.loadLayout(existingId), - `Non-unique layout with ID ${existingId}`); + expectPromiseRejectsWithError( + () => persistenceManager.loadLayout(existingId), + `Non-unique layout with ID ${existingId}` + ); }); }); describe("loadMetadata", () => { - it("retrieves array of persisted layout metadata", async () => { saveLocalEntity(metadataSaveLocation, [existingMetadata]); @@ -381,8 +465,4 @@ describe("loadMetadata", () => { it("returns empty array if no metadata is persisted", async () => { expect(await persistenceManager.loadMetadata()).toEqual([]); }); -}); - -const expectError = (f: () => Promise, message: string) => { - expect(f).rejects.toStrictEqual(new Error(message)); -}; +}); \ No newline at end of file diff --git a/vuu-ui/packages/vuu-layout/test/layout-persistence/RemoteLayoutPersistenceManager.test.ts b/vuu-ui/packages/vuu-layout/test/layout-persistence/RemoteLayoutPersistenceManager.test.ts new file mode 100644 index 000000000..4a3e41a16 --- /dev/null +++ b/vuu-ui/packages/vuu-layout/test/layout-persistence/RemoteLayoutPersistenceManager.test.ts @@ -0,0 +1,311 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { RemoteLayoutPersistenceManager } from "../../src/layout-persistence/RemoteLayoutPersistenceManager"; +import { LayoutMetadata } from "@finos/vuu-shell"; +import { LayoutJSON } from "../../src/layout-reducer"; +import { v4 as uuidv4 } from "uuid"; +import { expectPromiseRejectsWithError } from "./utils"; + +const persistence = new RemoteLayoutPersistenceManager(); +const mockFetch = vi.fn(); + +global.fetch = mockFetch; + +const metadata: LayoutMetadata[] = [ + { + id: "0001", + name: "layout 1", + group: "group 1", + screenshot: "", + user: "username", + created: "01.01.2000", + }, +]; + +const metadataToAdd: Omit = { + name: "layout 1", + group: "group 1", + screenshot: "", + user: "username", +}; + +const layout: LayoutJSON = { + type: "", +}; + +const uniqueId = uuidv4(); +const dateString = new Date().toISOString(); +const fetchError = new Error("Something went wrong with your request") + +describe("RemoteLayoutPersistenceManager", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe("createLayout", () => { + const responseJSON = { + metadata: { + ...metadataToAdd, + id: uniqueId, + created: dateString, + }, + }; + + it("resolves with metadata when fetch resolves, response is ok and contains metadata", () => { + const fetchResponse = { + json: () => new Promise((resolve) => resolve(responseJSON)), + ok: true, + }; + + mockFetch.mockResolvedValue(fetchResponse); + + const result = persistence.createLayout(metadataToAdd, layout); + + expect(result).resolves.toStrictEqual(responseJSON.metadata); + }); + + it("rejects with error when response is not ok", () => { + const errorMessage = "Not Found"; + + const fetchResponse = { + json: () => new Promise((resolve) => resolve(responseJSON)), + ok: false, + statusText: errorMessage, + }; + + mockFetch.mockResolvedValue(fetchResponse); + + expectPromiseRejectsWithError( + () => persistence.createLayout(metadata[0], layout), + errorMessage + ); + }); + + it("rejects with error when metadata in response is falsey", () => { + const fetchResponse = { + json: () => new Promise((resolve) => resolve({})), + ok: true, + }; + + mockFetch.mockResolvedValue(fetchResponse); + + expectPromiseRejectsWithError( + () => persistence.createLayout(metadata[0], layout), + "invalid metadata" + ); + }); + + it("rejects with error when fetch rejects", () => { + mockFetch.mockRejectedValue(fetchError); + + expectPromiseRejectsWithError( + () => persistence.createLayout(metadata[0], layout), + fetchError.message + ); + }); + }); + + describe("updateLayout", () => { + it("resolves when fetch resolves and response is ok", () => { + const fetchResponse = { + ok: true, + }; + + mockFetch.mockResolvedValue(fetchResponse); + + const result = persistence.updateLayout(uniqueId, metadata[0], layout); + + expect(result).resolves.toBe(undefined); + }); + + it("rejects with error when response is not ok", () => { + const errorMessage = "Not Found"; + + const fetchResponse = { + ok: false, + statusText: errorMessage, + }; + + mockFetch.mockResolvedValue(fetchResponse); + + expectPromiseRejectsWithError( + () => persistence.updateLayout(uniqueId, metadata[0], layout), + errorMessage + ); + }); + + it("rejects with error when fetch rejects", () => { + mockFetch.mockRejectedValue(fetchError); + + expectPromiseRejectsWithError( + () => persistence.updateLayout(uniqueId, metadata[0], layout), + fetchError.message + ); + }); + }); + + describe("deleteLayout", () => { + it("resolves when fetch resolves and response is ok", () => { + const fetchResponse = { + ok: true, + }; + + mockFetch.mockResolvedValue(fetchResponse); + + const result = persistence.deleteLayout(uniqueId); + + expect(result).resolves.toBe(undefined); + }); + + it("rejects with error when response is not ok", () => { + const errorMessage = "Not Found"; + + const fetchResponse = { + ok: false, + statusText: errorMessage, + }; + + mockFetch.mockResolvedValue(fetchResponse); + + expectPromiseRejectsWithError( + () => persistence.deleteLayout(uniqueId), + errorMessage + ); + }); + + it("rejects with error when fetch rejects", () => { + mockFetch.mockRejectedValue(fetchError); + + expectPromiseRejectsWithError( + () => persistence.deleteLayout(uniqueId), + fetchError.message + ); + }); + }); + + describe("loadMetadata", () => { + it("resolves with array of metadata when response is ok", () => { + const persistence = new RemoteLayoutPersistenceManager(); + + const fetchResponse = { + json: () => new Promise((resolve) => resolve(metadata)), + ok: true, + }; + + mockFetch.mockResolvedValue(fetchResponse); + + const result = persistence.loadMetadata(); + + expect(result).resolves.toBe(metadata); + }); + + it("rejects with error when response is not ok", () => { + const persistence = new RemoteLayoutPersistenceManager(); + + const errorMessage = "Not Found"; + + const fetchResponse = { + json: () => new Promise((resolve) => resolve(undefined)), + ok: false, + statusText: errorMessage, + }; + + mockFetch.mockResolvedValue(fetchResponse); + + expectPromiseRejectsWithError( + () => persistence.loadMetadata(), + errorMessage + ); + }); + + it("rejects with error when metadata is falsey in response", () => { + const persistence = new RemoteLayoutPersistenceManager(); + + const fetchResponse = { + json: () => new Promise((resolve) => resolve(undefined)), + ok: true, + }; + + mockFetch.mockResolvedValue(fetchResponse); + + expectPromiseRejectsWithError( + () => persistence.loadMetadata(), + "invalid metadata" + ); + }); + + it("rejects with error when fetch rejects", () => { + const persistence = new RemoteLayoutPersistenceManager(); + + mockFetch.mockRejectedValue(fetchError); + + expectPromiseRejectsWithError( + () => persistence.loadMetadata(), + fetchError.message + ); + }); + }); + + describe("loadLayout", () => { + it("resolves with array of metadata when response is ok", () => { + const persistence = new RemoteLayoutPersistenceManager(); + + const fetchResponse = { + json: () => new Promise((resolve) => resolve(layout)), + ok: true, + }; + + mockFetch.mockResolvedValue(fetchResponse); + + const result = persistence.loadLayout(uniqueId); + + expect(result).resolves.toBe(layout); + }); + + it("rejects with error when response is not ok", () => { + const persistence = new RemoteLayoutPersistenceManager(); + + const errorMessage = "Not Found"; + + const fetchResponse = { + json: () => new Promise((resolve) => resolve(undefined)), + ok: false, + statusText: errorMessage, + }; + + mockFetch.mockResolvedValue(fetchResponse); + + expectPromiseRejectsWithError( + () => persistence.loadLayout(uniqueId), + errorMessage + ); + }); + + it("rejects with error when metadata is falsey in response", () => { + const persistence = new RemoteLayoutPersistenceManager(); + + const fetchResponse = { + json: () => + new Promise((resolve: (value?: unknown) => void) => resolve()), + ok: true, + }; + + mockFetch.mockResolvedValue(fetchResponse); + + expectPromiseRejectsWithError( + () => persistence.loadLayout(uniqueId), + "invalid layout" + ); + }); + + it("rejects with error when fetch rejects", () => { + const persistence = new RemoteLayoutPersistenceManager(); + + mockFetch.mockRejectedValue(fetchError); + + expectPromiseRejectsWithError( + () => persistence.loadLayout(uniqueId), + fetchError.message + ); + }); + }); +}); diff --git a/vuu-ui/packages/vuu-layout/test/layout-persistence/utils.ts b/vuu-ui/packages/vuu-layout/test/layout-persistence/utils.ts new file mode 100644 index 000000000..1dff87194 --- /dev/null +++ b/vuu-ui/packages/vuu-layout/test/layout-persistence/utils.ts @@ -0,0 +1,8 @@ +import { expect } from "vitest"; + +export const expectPromiseRejectsWithError = ( + f: () => Promise, + message: string +) => { + expect(f).rejects.toStrictEqual(new Error(message)); +}; diff --git a/vuu-ui/packages/vuu-shell/src/layout-management/SaveLayoutPanel.tsx b/vuu-ui/packages/vuu-shell/src/layout-management/SaveLayoutPanel.tsx index 14b7e565b..3f9d959b5 100644 --- a/vuu-ui/packages/vuu-shell/src/layout-management/SaveLayoutPanel.tsx +++ b/vuu-ui/packages/vuu-shell/src/layout-management/SaveLayoutPanel.tsx @@ -1,7 +1,7 @@ import { ChangeEvent, useEffect, useState } from "react"; import { Input, Button, FormField, FormFieldLabel, Text } from "@salt-ds/core"; import { ComboBox, Checkbox, RadioButton } from "@finos/vuu-ui-controls"; -import { formatDate, takeScreenshot } from "@finos/vuu-utils"; +import { takeScreenshot } from "@finos/vuu-utils"; import { LayoutMetadata } from "./layoutTypes"; import "./SaveLayoutPanel.css"; @@ -33,7 +33,7 @@ type RadioValue = typeof radioValues[number]; type SaveLayoutPanelProps = { onCancel: () => void; - onSave: (layoutMetadata: Omit) => void; + onSave: (layoutMetadata: Omit) => void; componentId?: string }; @@ -59,8 +59,7 @@ export const SaveLayoutPanel = (props: SaveLayoutPanelProps) => { name: layoutName, group, screenshot: screenshot ?? "", - user: "User", - date: formatDate(new Date(), "dd.mm.yyyy") + user: "User" }) } diff --git a/vuu-ui/packages/vuu-shell/src/layout-management/layoutTypes.ts b/vuu-ui/packages/vuu-shell/src/layout-management/layoutTypes.ts index 96e0441cc..9c8ac1b27 100644 --- a/vuu-ui/packages/vuu-shell/src/layout-management/layoutTypes.ts +++ b/vuu-ui/packages/vuu-shell/src/layout-management/layoutTypes.ts @@ -9,7 +9,7 @@ export interface LayoutMetadata extends WithId { group: string; screenshot: string; user: string; - date: string; + created: string; } export interface Layout extends WithId { diff --git a/vuu-ui/packages/vuu-shell/src/layout-management/useLayoutManager.tsx b/vuu-ui/packages/vuu-shell/src/layout-management/useLayoutManager.tsx index b00440d62..b87f8a43f 100644 --- a/vuu-ui/packages/vuu-shell/src/layout-management/useLayoutManager.tsx +++ b/vuu-ui/packages/vuu-shell/src/layout-management/useLayoutManager.tsx @@ -1,21 +1,23 @@ import React, { useState, useCallback, useContext, useEffect } from "react"; -import { LayoutJSON, LocalLayoutPersistenceManager, resolveJSONPath } from "@finos/vuu-layout"; +import { LayoutJSON, LocalLayoutPersistenceManager, resolveJSONPath, RemoteLayoutPersistenceManager } from "@finos/vuu-layout"; import { LayoutMetadata } from "./layoutTypes"; import { defaultLayout } from "@finos/vuu-layout/"; -const persistenceManager = new LocalLayoutPersistenceManager(); +const local = process.env.LOCAL || false; + +const persistenceManager = local ? new LocalLayoutPersistenceManager() : new RemoteLayoutPersistenceManager(); export const LayoutManagementContext = React.createContext<{ layoutMetadata: LayoutMetadata[], - saveLayout: (n: Omit) => void, + saveLayout: (n: Omit) => void, applicationLayout: LayoutJSON, saveApplicationLayout: (layout: LayoutJSON) => void, loadLayoutById: (id: string) => void }>({ layoutMetadata: [], - saveLayout: () => { }, + saveLayout: () => null, applicationLayout: defaultLayout, - saveApplicationLayout: () => { }, + saveApplicationLayout: () => null, loadLayoutById: () => defaultLayout }) @@ -34,6 +36,10 @@ export const LayoutManagementProvider = (props: LayoutManagementProviderProps) = persistenceManager.loadApplicationLayout().then(layout => { setApplicationLayout(layout); }) + .catch((error: Error) => { + //TODO: Show error toaster + console.error("Error occurred while retrieving metadata", error) + }) }, []) const saveApplicationLayout = useCallback((layout: LayoutJSON) => { @@ -41,18 +47,17 @@ export const LayoutManagementProvider = (props: LayoutManagementProviderProps) = persistenceManager.saveApplicationLayout(layout) }, []); - const saveLayout = useCallback((metadata: Omit) => { + const saveLayout = useCallback((metadata:Omit) => { const layoutToSave = resolveJSONPath(applicationLayout, "#main-tabs.ACTIVE_CHILD"); if (layoutToSave) { - persistenceManager.createLayout(metadata, layoutToSave).then(generatedId => { - const newMetadata: LayoutMetadata = { - ...metadata, - id: generatedId - }; - - setLayoutMetadata(prev => [...prev, newMetadata]); + persistenceManager.createLayout(metadata, layoutToSave).then((metadata) => { + setLayoutMetadata(prev => [...prev, metadata]); + //TODO: Show success toast + }).catch((error: Error) => { + //TODO: Show error toaster + console.error("Error occurred while saving layout", error) }) } //TODO else{ show error message} diff --git a/vuu-ui/showcase/src/examples/Apps/NewTheme.examples.tsx b/vuu-ui/showcase/src/examples/Apps/NewTheme.examples.tsx index bc1910645..71bb1bff5 100644 --- a/vuu-ui/showcase/src/examples/Apps/NewTheme.examples.tsx +++ b/vuu-ui/showcase/src/examples/Apps/NewTheme.examples.tsx @@ -116,7 +116,7 @@ const ShellWithNewTheme = () => { const { saveLayout } = useLayoutManager(); const handleSave = useCallback( - (layoutMetadata: Omit) => { + (layoutMetadata: Omit) => { saveLayout(layoutMetadata); setDialogContent(undefined); }, diff --git a/vuu-ui/showcase/vite.config.js b/vuu-ui/showcase/vite.config.js index b524f35b7..508d4adc8 100644 --- a/vuu-ui/showcase/vite.config.js +++ b/vuu-ui/showcase/vite.config.js @@ -7,6 +7,7 @@ export default defineConfig({ }, define: { "process.env.NODE_DEBUG": false, + "process.env.LOCAL": true, }, esbuild: { jsx: `automatic`, From eecde0bfd5b5788d1204d897c60b54c287b21e48 Mon Sep 17 00:00:00 2001 From: vferraro-scottlogic Date: Mon, 16 Oct 2023 11:19:51 +0100 Subject: [PATCH 02/56] VUU-27 https in baseUrl --- .../src/layout-persistence/RemoteLayoutPersistenceManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts b/vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts index ca63c8689..d0a9ec98f 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts @@ -3,7 +3,7 @@ import { LayoutPersistenceManager } from "./LayoutPersistenceManager"; import { LayoutJSON } from "../layout-reducer"; import { defaultLayout } from "./data"; -const baseURL = "http://127.0.0.1:8081/api"; +const baseURL = "https://127.0.0.1:8081/api"; const metadataSaveLocation = "layouts/metadata"; const layoutsSaveLocation = "layouts"; From 94ab0bb7e22551d4a51e31fde70fe37ffbc37253 Mon Sep 17 00:00:00 2001 From: vferraro-scottlogic Date: Wed, 18 Oct 2023 11:14:17 +0100 Subject: [PATCH 03/56] VUU-27 add cors config --- .semgrepignore | 1 + .../org/finos/vuu/layoutserver/CorsConfig.java | 15 +++++++++++++++ .../RemoteLayoutPersistenceManager.ts | 2 +- 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/CorsConfig.java diff --git a/.semgrepignore b/.semgrepignore index 1c19645e3..5b374e053 100644 --- a/.semgrepignore +++ b/.semgrepignore @@ -4,6 +4,7 @@ vuu/src/main/resources/www/ws-example.html vuu/src/main/scala/org/finos/vuu/provider/simulation/SimulatedBigInstrumentsProvider.scala vuu-ui/packages/vuu-data/src/array-data-source/group-utils.ts vuu-ui/packages/vuu-datagrid-extras/src/column-expression-input/column-language-parser/walkExpressionTree.ts +vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts vuu-ui/packages/vuu-popups/src/menu/useContextMenu.tsx vuu-ui/packages/vuu-table-extras/src/cell-edit-validators/PatternValidator.ts vuu-ui/packages/vuu-ui-controls/src/list/Highlighter.tsx diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/CorsConfig.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/CorsConfig.java new file mode 100644 index 000000000..7950487b4 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/CorsConfig.java @@ -0,0 +1,15 @@ +package org.finos.vuu.layoutserver; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class CorsConfig implements WebMvcConfigurer { + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins("http://127.0.0.1:5173") + .allowedMethods("GET", "POST", "PUT", "DELETE"); + } +} diff --git a/vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts b/vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts index d0a9ec98f..ca63c8689 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts @@ -3,7 +3,7 @@ import { LayoutPersistenceManager } from "./LayoutPersistenceManager"; import { LayoutJSON } from "../layout-reducer"; import { defaultLayout } from "./data"; -const baseURL = "https://127.0.0.1:8081/api"; +const baseURL = "http://127.0.0.1:8081/api"; const metadataSaveLocation = "layouts/metadata"; const layoutsSaveLocation = "layouts"; From 7034f4263e7de4aec02ea97545bff4af99853e88 Mon Sep 17 00:00:00 2001 From: vferraro-scottlogic Date: Fri, 20 Oct 2023 13:22:09 +0100 Subject: [PATCH 04/56] VUU-27 improve error messages --- .../layout-persistence/RemoteLayoutPersistenceManager.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts b/vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts index ca63c8689..06859ce7d 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts @@ -21,8 +21,8 @@ export class RemoteLayoutPersistenceManager }, method: "POST", body: JSON.stringify({ - metadata: { ...metadata }, - definition: JSON.stringify(layout), + metadata, + definition: layout, }), }) .then((response) => { @@ -31,7 +31,7 @@ export class RemoteLayoutPersistenceManager } response.json().then(({ metadata }: { metadata: LayoutMetadata }) => { if (!metadata) { - reject(new Error("invalid metadata")); + reject(new Error("Response did not contain valid metadata")); } resolve(metadata); }); @@ -93,7 +93,7 @@ export class RemoteLayoutPersistenceManager } response.json().then((layout) => { if (!layout) { - reject(new Error("invalid layout")); + reject(new Error("Response did not contain a valid layout")); } resolve(layout); }); From f5eb9aee41bc916bfb7631026d9d17f67795214d Mon Sep 17 00:00:00 2001 From: vferraro-scottlogic Date: Fri, 20 Oct 2023 13:24:26 +0100 Subject: [PATCH 05/56] VUU-27 improve error --- .../src/layout-persistence/RemoteLayoutPersistenceManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts b/vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts index 06859ce7d..9bdf5f50d 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts @@ -113,7 +113,7 @@ export class RemoteLayoutPersistenceManager } response.json().then((metadata: LayoutMetadata[]) => { if (!metadata) { - reject(new Error("invalid metadata")); + reject(new Error("Response did not contain valid metadata")); } resolve(metadata); }); From a3a93b2ee7dfabb09f0d82f4608c98d9e296492e Mon Sep 17 00:00:00 2001 From: vferraro-scottlogic Date: Fri, 20 Oct 2023 14:49:48 +0100 Subject: [PATCH 06/56] VUU-27 add type for LayoutMetadataDto --- .../src/layout-persistence/LayoutPersistenceManager.ts | 4 ++-- .../src/layout-persistence/LocalLayoutPersistenceManager.ts | 4 ++-- .../layout-persistence/RemoteLayoutPersistenceManager.ts | 4 ++-- .../LocalLayoutPersistenceManager.test.ts | 4 ++-- .../RemoteLayoutPersistenceManager.test.ts | 6 +++--- .../vuu-shell/src/layout-management/SaveLayoutPanel.tsx | 4 ++-- .../packages/vuu-shell/src/layout-management/layoutTypes.ts | 2 ++ .../vuu-shell/src/layout-management/useLayoutManager.tsx | 6 +++--- vuu-ui/showcase/src/examples/Apps/NewTheme.examples.tsx | 4 ++-- 9 files changed, 20 insertions(+), 18 deletions(-) diff --git a/vuu-ui/packages/vuu-layout/src/layout-persistence/LayoutPersistenceManager.ts b/vuu-ui/packages/vuu-layout/src/layout-persistence/LayoutPersistenceManager.ts index 39d8be719..3fba7fecc 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-persistence/LayoutPersistenceManager.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-persistence/LayoutPersistenceManager.ts @@ -1,5 +1,5 @@ import { LayoutJSON } from "@finos/vuu-layout"; -import { LayoutMetadata } from "@finos/vuu-shell"; +import { LayoutMetadata, LayoutMetadataDto } from "@finos/vuu-shell"; export interface LayoutPersistenceManager { /** @@ -10,7 +10,7 @@ export interface LayoutPersistenceManager { * * @returns Unique identifier assigned to the saved layout */ - createLayout: (metadata: Omit, layout: LayoutJSON) => Promise; + createLayout: (metadata: LayoutMetadataDto, layout: LayoutJSON) => Promise; /** * Overwrites an existing layout and its corresponding metadata with the provided information diff --git a/vuu-ui/packages/vuu-layout/src/layout-persistence/LocalLayoutPersistenceManager.ts b/vuu-ui/packages/vuu-layout/src/layout-persistence/LocalLayoutPersistenceManager.ts index 2d1f9603b..1d0629c76 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-persistence/LocalLayoutPersistenceManager.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-persistence/LocalLayoutPersistenceManager.ts @@ -1,4 +1,4 @@ -import { Layout, LayoutMetadata, WithId } from "@finos/vuu-shell"; +import { Layout, LayoutMetadata, LayoutMetadataDto, WithId } from "@finos/vuu-shell"; import { getLocalEntity, saveLocalEntity } from "@finos/vuu-filters"; import { formatDate, getUniqueId } from "@finos/vuu-utils"; @@ -11,7 +11,7 @@ const layoutsSaveLocation = "layouts/layouts"; export class LocalLayoutPersistenceManager implements LayoutPersistenceManager { createLayout( - metadata: Omit, + metadata: LayoutMetadataDto, layout: LayoutJSON ): Promise { return new Promise((resolve) => { diff --git a/vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts b/vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts index 9bdf5f50d..a90c9d491 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts @@ -1,4 +1,4 @@ -import { LayoutMetadata } from "@finos/vuu-shell"; +import { LayoutMetadata, LayoutMetadataDto } from "@finos/vuu-shell"; import { LayoutPersistenceManager } from "./LayoutPersistenceManager"; import { LayoutJSON } from "../layout-reducer"; import { defaultLayout } from "./data"; @@ -11,7 +11,7 @@ export class RemoteLayoutPersistenceManager implements LayoutPersistenceManager { createLayout( - metadata: Omit, + metadata: LayoutMetadataDto, layout: LayoutJSON ): Promise { return new Promise((resolve, reject) => diff --git a/vuu-ui/packages/vuu-layout/test/layout-persistence/LocalLayoutPersistenceManager.test.ts b/vuu-ui/packages/vuu-layout/test/layout-persistence/LocalLayoutPersistenceManager.test.ts index 41a58644c..bb0a5bdea 100644 --- a/vuu-ui/packages/vuu-layout/test/layout-persistence/LocalLayoutPersistenceManager.test.ts +++ b/vuu-ui/packages/vuu-layout/test/layout-persistence/LocalLayoutPersistenceManager.test.ts @@ -1,4 +1,4 @@ -import { Layout, LayoutMetadata } from "@finos/vuu-shell"; +import { Layout, LayoutMetadata, LayoutMetadataDto } from "@finos/vuu-shell"; import { afterEach, describe, expect, it, vi } from "vitest"; import { LocalLayoutPersistenceManager } from "../../src/layout-persistence"; import { LayoutJSON } from "../../src/layout-reducer"; @@ -43,7 +43,7 @@ const existingLayout: Layout = { json: { type: "t0" }, }; -const metadataToAdd: Omit = { +const metadataToAdd: LayoutMetadataDto = { name: "New Layout", group: "Group 1", screenshot: "screenshot", diff --git a/vuu-ui/packages/vuu-layout/test/layout-persistence/RemoteLayoutPersistenceManager.test.ts b/vuu-ui/packages/vuu-layout/test/layout-persistence/RemoteLayoutPersistenceManager.test.ts index 4a3e41a16..2b58f66ba 100644 --- a/vuu-ui/packages/vuu-layout/test/layout-persistence/RemoteLayoutPersistenceManager.test.ts +++ b/vuu-ui/packages/vuu-layout/test/layout-persistence/RemoteLayoutPersistenceManager.test.ts @@ -90,7 +90,7 @@ describe("RemoteLayoutPersistenceManager", () => { expectPromiseRejectsWithError( () => persistence.createLayout(metadata[0], layout), - "invalid metadata" + "Response did not contain valid metadata" ); }); @@ -229,7 +229,7 @@ describe("RemoteLayoutPersistenceManager", () => { expectPromiseRejectsWithError( () => persistence.loadMetadata(), - "invalid metadata" + "Response did not contain valid metadata" ); }); @@ -293,7 +293,7 @@ describe("RemoteLayoutPersistenceManager", () => { expectPromiseRejectsWithError( () => persistence.loadLayout(uniqueId), - "invalid layout" + "Response did not contain a valid layout" ); }); diff --git a/vuu-ui/packages/vuu-shell/src/layout-management/SaveLayoutPanel.tsx b/vuu-ui/packages/vuu-shell/src/layout-management/SaveLayoutPanel.tsx index 3f9d959b5..65118435f 100644 --- a/vuu-ui/packages/vuu-shell/src/layout-management/SaveLayoutPanel.tsx +++ b/vuu-ui/packages/vuu-shell/src/layout-management/SaveLayoutPanel.tsx @@ -2,7 +2,7 @@ import { ChangeEvent, useEffect, useState } from "react"; import { Input, Button, FormField, FormFieldLabel, Text } from "@salt-ds/core"; import { ComboBox, Checkbox, RadioButton } from "@finos/vuu-ui-controls"; import { takeScreenshot } from "@finos/vuu-utils"; -import { LayoutMetadata } from "./layoutTypes"; +import { LayoutMetadataDto } from "./layoutTypes"; import "./SaveLayoutPanel.css"; @@ -33,7 +33,7 @@ type RadioValue = typeof radioValues[number]; type SaveLayoutPanelProps = { onCancel: () => void; - onSave: (layoutMetadata: Omit) => void; + onSave: (layoutMetadata: LayoutMetadataDto) => void; componentId?: string }; diff --git a/vuu-ui/packages/vuu-shell/src/layout-management/layoutTypes.ts b/vuu-ui/packages/vuu-shell/src/layout-management/layoutTypes.ts index 9c8ac1b27..8b2bf1dd3 100644 --- a/vuu-ui/packages/vuu-shell/src/layout-management/layoutTypes.ts +++ b/vuu-ui/packages/vuu-shell/src/layout-management/layoutTypes.ts @@ -12,6 +12,8 @@ export interface LayoutMetadata extends WithId { created: string; } +export type LayoutMetadataDto = Omit; + export interface Layout extends WithId { json: LayoutJSON; } diff --git a/vuu-ui/packages/vuu-shell/src/layout-management/useLayoutManager.tsx b/vuu-ui/packages/vuu-shell/src/layout-management/useLayoutManager.tsx index b87f8a43f..28c8a6df7 100644 --- a/vuu-ui/packages/vuu-shell/src/layout-management/useLayoutManager.tsx +++ b/vuu-ui/packages/vuu-shell/src/layout-management/useLayoutManager.tsx @@ -1,6 +1,6 @@ import React, { useState, useCallback, useContext, useEffect } from "react"; import { LayoutJSON, LocalLayoutPersistenceManager, resolveJSONPath, RemoteLayoutPersistenceManager } from "@finos/vuu-layout"; -import { LayoutMetadata } from "./layoutTypes"; +import { LayoutMetadata, LayoutMetadataDto } from "./layoutTypes"; import { defaultLayout } from "@finos/vuu-layout/"; const local = process.env.LOCAL || false; @@ -9,7 +9,7 @@ const persistenceManager = local ? new LocalLayoutPersistenceManager() : new Rem export const LayoutManagementContext = React.createContext<{ layoutMetadata: LayoutMetadata[], - saveLayout: (n: Omit) => void, + saveLayout: (n: LayoutMetadataDto) => void, applicationLayout: LayoutJSON, saveApplicationLayout: (layout: LayoutJSON) => void, loadLayoutById: (id: string) => void @@ -47,7 +47,7 @@ export const LayoutManagementProvider = (props: LayoutManagementProviderProps) = persistenceManager.saveApplicationLayout(layout) }, []); - const saveLayout = useCallback((metadata:Omit) => { + const saveLayout = useCallback((metadata:LayoutMetadataDto) => { const layoutToSave = resolveJSONPath(applicationLayout, "#main-tabs.ACTIVE_CHILD"); diff --git a/vuu-ui/showcase/src/examples/Apps/NewTheme.examples.tsx b/vuu-ui/showcase/src/examples/Apps/NewTheme.examples.tsx index 71bb1bff5..5711f461a 100644 --- a/vuu-ui/showcase/src/examples/Apps/NewTheme.examples.tsx +++ b/vuu-ui/showcase/src/examples/Apps/NewTheme.examples.tsx @@ -14,7 +14,7 @@ import { FeatureConfig, FeatureProps, LayoutManagementProvider, - LayoutMetadata, + LayoutMetadataDto, LeftNav, SaveLayoutPanel, Shell, @@ -116,7 +116,7 @@ const ShellWithNewTheme = () => { const { saveLayout } = useLayoutManager(); const handleSave = useCallback( - (layoutMetadata: Omit) => { + (layoutMetadata: LayoutMetadataDto) => { saveLayout(layoutMetadata); setDialogContent(undefined); }, From 18ec1cf75663281287a31865d0bdcde249b847d6 Mon Sep 17 00:00:00 2001 From: vferraro-scottlogic Date: Fri, 20 Oct 2023 14:57:56 +0100 Subject: [PATCH 07/56] VUU-27 remove redundant instantiation of persistenceManager --- .../RemoteLayoutPersistenceManager.ts | 2 +- .../RemoteLayoutPersistenceManager.test.ts | 18 +----------------- 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts b/vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts index a90c9d491..f303813f4 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts @@ -44,7 +44,7 @@ export class RemoteLayoutPersistenceManager updateLayout( id: string, - metadata: Omit, + metadata: LayoutMetadataDto, newLayoutJson: LayoutJSON ): Promise { return new Promise((resolve, reject) => diff --git a/vuu-ui/packages/vuu-layout/test/layout-persistence/RemoteLayoutPersistenceManager.test.ts b/vuu-ui/packages/vuu-layout/test/layout-persistence/RemoteLayoutPersistenceManager.test.ts index 2b58f66ba..72bb6fe72 100644 --- a/vuu-ui/packages/vuu-layout/test/layout-persistence/RemoteLayoutPersistenceManager.test.ts +++ b/vuu-ui/packages/vuu-layout/test/layout-persistence/RemoteLayoutPersistenceManager.test.ts @@ -34,7 +34,7 @@ const layout: LayoutJSON = { const uniqueId = uuidv4(); const dateString = new Date().toISOString(); -const fetchError = new Error("Something went wrong with your request") +const fetchError = new Error("Something went wrong with your request"); describe("RemoteLayoutPersistenceManager", () => { beforeEach(() => { @@ -184,8 +184,6 @@ describe("RemoteLayoutPersistenceManager", () => { describe("loadMetadata", () => { it("resolves with array of metadata when response is ok", () => { - const persistence = new RemoteLayoutPersistenceManager(); - const fetchResponse = { json: () => new Promise((resolve) => resolve(metadata)), ok: true, @@ -199,8 +197,6 @@ describe("RemoteLayoutPersistenceManager", () => { }); it("rejects with error when response is not ok", () => { - const persistence = new RemoteLayoutPersistenceManager(); - const errorMessage = "Not Found"; const fetchResponse = { @@ -218,8 +214,6 @@ describe("RemoteLayoutPersistenceManager", () => { }); it("rejects with error when metadata is falsey in response", () => { - const persistence = new RemoteLayoutPersistenceManager(); - const fetchResponse = { json: () => new Promise((resolve) => resolve(undefined)), ok: true, @@ -234,8 +228,6 @@ describe("RemoteLayoutPersistenceManager", () => { }); it("rejects with error when fetch rejects", () => { - const persistence = new RemoteLayoutPersistenceManager(); - mockFetch.mockRejectedValue(fetchError); expectPromiseRejectsWithError( @@ -247,8 +239,6 @@ describe("RemoteLayoutPersistenceManager", () => { describe("loadLayout", () => { it("resolves with array of metadata when response is ok", () => { - const persistence = new RemoteLayoutPersistenceManager(); - const fetchResponse = { json: () => new Promise((resolve) => resolve(layout)), ok: true, @@ -262,8 +252,6 @@ describe("RemoteLayoutPersistenceManager", () => { }); it("rejects with error when response is not ok", () => { - const persistence = new RemoteLayoutPersistenceManager(); - const errorMessage = "Not Found"; const fetchResponse = { @@ -281,8 +269,6 @@ describe("RemoteLayoutPersistenceManager", () => { }); it("rejects with error when metadata is falsey in response", () => { - const persistence = new RemoteLayoutPersistenceManager(); - const fetchResponse = { json: () => new Promise((resolve: (value?: unknown) => void) => resolve()), @@ -298,8 +284,6 @@ describe("RemoteLayoutPersistenceManager", () => { }); it("rejects with error when fetch rejects", () => { - const persistence = new RemoteLayoutPersistenceManager(); - mockFetch.mockRejectedValue(fetchError); expectPromiseRejectsWithError( From 847f7c1f55435e457ce3eca471edcdbe8aea0192 Mon Sep 17 00:00:00 2001 From: vferraro-scottlogic Date: Fri, 20 Oct 2023 15:51:54 +0100 Subject: [PATCH 08/56] VUU-27 improvements to unit tests --- .../RemoteLayoutPersistenceManager.test.ts | 51 +++++++++++-------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/vuu-ui/packages/vuu-layout/test/layout-persistence/RemoteLayoutPersistenceManager.test.ts b/vuu-ui/packages/vuu-layout/test/layout-persistence/RemoteLayoutPersistenceManager.test.ts index 72bb6fe72..6c5c4868e 100644 --- a/vuu-ui/packages/vuu-layout/test/layout-persistence/RemoteLayoutPersistenceManager.test.ts +++ b/vuu-ui/packages/vuu-layout/test/layout-persistence/RemoteLayoutPersistenceManager.test.ts @@ -24,25 +24,35 @@ const metadata: LayoutMetadata[] = [ const metadataToAdd: Omit = { name: "layout 1", group: "group 1", - screenshot: "", + screenshot: "screenshot", user: "username", }; const layout: LayoutJSON = { - type: "", + type: "View", }; const uniqueId = uuidv4(); const dateString = new Date().toISOString(); const fetchError = new Error("Something went wrong with your request"); +type FetchResponse = { + json?: () => Promise; + ok: boolean; + statusText?: string; +}; + +type CreateLayoutResponseJSON = { + metadata: LayoutMetadata; +}; + describe("RemoteLayoutPersistenceManager", () => { beforeEach(() => { vi.clearAllMocks(); }); describe("createLayout", () => { - const responseJSON = { + const responseJSON: CreateLayoutResponseJSON = { metadata: { ...metadataToAdd, id: uniqueId, @@ -51,7 +61,7 @@ describe("RemoteLayoutPersistenceManager", () => { }; it("resolves with metadata when fetch resolves, response is ok and contains metadata", () => { - const fetchResponse = { + const fetchResponse: FetchResponse = { json: () => new Promise((resolve) => resolve(responseJSON)), ok: true, }; @@ -66,7 +76,7 @@ describe("RemoteLayoutPersistenceManager", () => { it("rejects with error when response is not ok", () => { const errorMessage = "Not Found"; - const fetchResponse = { + const fetchResponse: FetchResponse = { json: () => new Promise((resolve) => resolve(responseJSON)), ok: false, statusText: errorMessage, @@ -81,7 +91,7 @@ describe("RemoteLayoutPersistenceManager", () => { }); it("rejects with error when metadata in response is falsey", () => { - const fetchResponse = { + const fetchResponse: FetchResponse = { json: () => new Promise((resolve) => resolve({})), ok: true, }; @@ -106,7 +116,7 @@ describe("RemoteLayoutPersistenceManager", () => { describe("updateLayout", () => { it("resolves when fetch resolves and response is ok", () => { - const fetchResponse = { + const fetchResponse: FetchResponse = { ok: true, }; @@ -120,7 +130,7 @@ describe("RemoteLayoutPersistenceManager", () => { it("rejects with error when response is not ok", () => { const errorMessage = "Not Found"; - const fetchResponse = { + const fetchResponse: FetchResponse = { ok: false, statusText: errorMessage, }; @@ -145,7 +155,7 @@ describe("RemoteLayoutPersistenceManager", () => { describe("deleteLayout", () => { it("resolves when fetch resolves and response is ok", () => { - const fetchResponse = { + const fetchResponse: FetchResponse = { ok: true, }; @@ -159,7 +169,7 @@ describe("RemoteLayoutPersistenceManager", () => { it("rejects with error when response is not ok", () => { const errorMessage = "Not Found"; - const fetchResponse = { + const fetchResponse: FetchResponse = { ok: false, statusText: errorMessage, }; @@ -184,7 +194,7 @@ describe("RemoteLayoutPersistenceManager", () => { describe("loadMetadata", () => { it("resolves with array of metadata when response is ok", () => { - const fetchResponse = { + const fetchResponse: FetchResponse = { json: () => new Promise((resolve) => resolve(metadata)), ok: true, }; @@ -199,8 +209,8 @@ describe("RemoteLayoutPersistenceManager", () => { it("rejects with error when response is not ok", () => { const errorMessage = "Not Found"; - const fetchResponse = { - json: () => new Promise((resolve) => resolve(undefined)), + const fetchResponse: FetchResponse = { + json: () => new Promise((resolve) => resolve()), ok: false, statusText: errorMessage, }; @@ -214,8 +224,8 @@ describe("RemoteLayoutPersistenceManager", () => { }); it("rejects with error when metadata is falsey in response", () => { - const fetchResponse = { - json: () => new Promise((resolve) => resolve(undefined)), + const fetchResponse: FetchResponse = { + json: () => new Promise((resolve) => resolve()), ok: true, }; @@ -239,7 +249,7 @@ describe("RemoteLayoutPersistenceManager", () => { describe("loadLayout", () => { it("resolves with array of metadata when response is ok", () => { - const fetchResponse = { + const fetchResponse: FetchResponse = { json: () => new Promise((resolve) => resolve(layout)), ok: true, }; @@ -254,8 +264,8 @@ describe("RemoteLayoutPersistenceManager", () => { it("rejects with error when response is not ok", () => { const errorMessage = "Not Found"; - const fetchResponse = { - json: () => new Promise((resolve) => resolve(undefined)), + const fetchResponse: FetchResponse = { + json: () => new Promise((resolve) => resolve()), ok: false, statusText: errorMessage, }; @@ -269,9 +279,8 @@ describe("RemoteLayoutPersistenceManager", () => { }); it("rejects with error when metadata is falsey in response", () => { - const fetchResponse = { - json: () => - new Promise((resolve: (value?: unknown) => void) => resolve()), + const fetchResponse: FetchResponse = { + json: () => new Promise((resolve) => resolve()), ok: true, }; From 20cead2d53ca487a976823933a525003c7fdc2da Mon Sep 17 00:00:00 2001 From: vferraro-scottlogic Date: Fri, 20 Oct 2023 16:33:31 +0100 Subject: [PATCH 09/56] VUU-27 make fetch method explicit for "GET" --- .../RemoteLayoutPersistenceManager.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts b/vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts index f303813f4..06f6fa4ba 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts @@ -86,7 +86,9 @@ export class RemoteLayoutPersistenceManager loadLayout(id: string): Promise { return new Promise((resolve, reject) => { - fetch(`${baseURL}/${layoutsSaveLocation}/${id}`, {}) + fetch(`${baseURL}/${layoutsSaveLocation}/${id}`, { + method: "GET", + }) .then((response) => { if (!response.ok) { reject(new Error(response.statusText)); @@ -106,8 +108,10 @@ export class RemoteLayoutPersistenceManager loadMetadata(): Promise { return new Promise((resolve, reject) => - fetch(`${baseURL}/${metadataSaveLocation}`, {}) - .then(async (response) => { + fetch(`${baseURL}/${metadataSaveLocation}`, { + method: "GET", + }) + .then((response) => { if (!response.ok) { reject(new Error(response.statusText)); } From e097743401338eef686eb1a873d69ac4991c99f1 Mon Sep 17 00:00:00 2001 From: Peter Ling Date: Fri, 13 Oct 2023 15:03:53 +0100 Subject: [PATCH 10/56] VUU-70: Create resource for application layouts --- .../ApplicationLayoutController.java | 71 +++++++++ .../dto/ApplicationLayoutDto.java | 25 +++ .../layoutserver/model/ApplicationLayout.java | 31 ++++ .../ApplicationLayoutRepository.java | 9 ++ .../service/ApplicationLayoutService.java | 82 ++++++++++ .../src/main/resources/defaultLayout.json | 22 +++ .../service/ApplicationLayoutServiceTest.java | 145 ++++++++++++++++++ 7 files changed, 385 insertions(+) create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/dto/ApplicationLayoutDto.java create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/model/ApplicationLayout.java create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/repository/ApplicationLayoutRepository.java create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java create mode 100644 layout-server/src/main/resources/defaultLayout.json create mode 100644 layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java new file mode 100644 index 000000000..540234630 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java @@ -0,0 +1,71 @@ +package org.finos.vuu.layoutserver.controller; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.RequiredArgsConstructor; +import org.finos.vuu.layoutserver.dto.ApplicationLayoutDto; +import org.finos.vuu.layoutserver.service.ApplicationLayoutService; +import org.springframework.http.HttpStatus; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/application-layouts") +@Validated +public class ApplicationLayoutController { + + private final ApplicationLayoutService service; + + /** + * Gets the application layout for the requesting user. Returns a default layout if none exists. + * + * @return the application layout + */ + @GetMapping + public ApplicationLayoutDto getApplicationLayout(@RequestHeader("user") String username) { + return service.getApplicationLayout(username); + } + + /** + * Creates a new application layout for the requesting user. + * + * @param layoutDefinition JSON representation of the application layout to be created + * @param username the user making the request + */ + @ResponseStatus(HttpStatus.CREATED) + @PostMapping + public void createLayout(@RequestHeader("user") String username, @RequestBody JsonNode layoutDefinition) { + service.createApplicationLayout(username, layoutDefinition); + } + + /** + * Updates the application layout for the requesting user. + * + * @param layoutDefinition JSON representation of the application layout to be created + * @param username the user making the request + */ + @ResponseStatus(HttpStatus.NO_CONTENT) + @PutMapping + public void updateLayout(@RequestHeader("user") String username, @RequestBody JsonNode layoutDefinition) { + service.updateApplicationLayout(username, layoutDefinition); + } + + /** + * Deletes the application layout for the requesting user. + * + * @param username the user making the request + */ + @ResponseStatus(HttpStatus.NO_CONTENT) + @DeleteMapping + public void deleteLayout(@RequestHeader("user") String username) { + service.deleteApplicationLayout(username); + } +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/ApplicationLayoutDto.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/ApplicationLayoutDto.java new file mode 100644 index 000000000..52e9455f1 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/ApplicationLayoutDto.java @@ -0,0 +1,25 @@ +package org.finos.vuu.layoutserver.dto; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.finos.vuu.layoutserver.model.ApplicationLayout; + +@Data +@RequiredArgsConstructor +@AllArgsConstructor +@Builder +public class ApplicationLayoutDto { + private String user; + private JsonNode definition; + + public static ApplicationLayoutDto fromEntity(ApplicationLayout entity) throws JsonProcessingException { + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode definition = objectMapper.readTree(entity.extractDefinition()); + return new ApplicationLayoutDto(entity.getUsername(), definition); + } +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/ApplicationLayout.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/ApplicationLayout.java new file mode 100644 index 000000000..192680359 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/ApplicationLayout.java @@ -0,0 +1,31 @@ +package org.finos.vuu.layoutserver.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.RequiredArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; + +@Data +@Entity +@RequiredArgsConstructor +@AllArgsConstructor +public class ApplicationLayout { + @Id + private String username; + + @Column(columnDefinition = "JSON") + private String definition; + + public String extractDefinition() { + String extractedDefinition = definition; + + if (extractedDefinition.startsWith("\"") && extractedDefinition.endsWith("\"")) { + extractedDefinition = extractedDefinition.substring(1, extractedDefinition.length() - 1); + } + + return extractedDefinition.replaceAll("\\\\", ""); + } +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/repository/ApplicationLayoutRepository.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/repository/ApplicationLayoutRepository.java new file mode 100644 index 000000000..c553e7751 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/repository/ApplicationLayoutRepository.java @@ -0,0 +1,9 @@ +package org.finos.vuu.layoutserver.repository; + +import org.finos.vuu.layoutserver.model.ApplicationLayout; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ApplicationLayoutRepository extends CrudRepository { +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java new file mode 100644 index 000000000..f20833a32 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java @@ -0,0 +1,82 @@ +package org.finos.vuu.layoutserver.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import org.finos.vuu.layoutserver.dto.ApplicationLayoutDto; +import org.finos.vuu.layoutserver.model.ApplicationLayout; +import org.finos.vuu.layoutserver.repository.ApplicationLayoutRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.io.ClassPathResource; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.util.NoSuchElementException; +import java.util.Optional; + +@RequiredArgsConstructor +@Service +public class ApplicationLayoutService { + + private static final Logger logger = LoggerFactory.getLogger(ApplicationLayoutService.class); + private static ApplicationLayoutDto defaultLayout; + private final ApplicationLayoutRepository repository; + + public void createApplicationLayout(String username, JsonNode layoutDefinition) { + repository.save(new ApplicationLayout(username, layoutDefinition.toString())); + } + + public ApplicationLayoutDto getApplicationLayout(String username) { + Optional layout = repository.findById(username); + + if (layout.isEmpty()) { + logger.info("No application layout for user, returning default"); + return getDefaultLayout(); + } + + try { + return ApplicationLayoutDto.fromEntity(layout.get()); + } catch (JsonProcessingException e) { + logger.warn("Failed to read user's application layout, returning default"); + return getDefaultLayout(); + } + } + + public void updateApplicationLayout(String username, JsonNode layoutDefinition) { + createApplicationLayout(username, layoutDefinition); + } + + public void deleteApplicationLayout(String username) { + try { + repository.deleteById(username); + } catch (EmptyResultDataAccessException e) { + throw new NoSuchElementException("No layout found for user: " + username); + } + } + + private ApplicationLayoutDto getDefaultLayout() { + if (defaultLayout == null) { + loadDefaultLayout(); + } + return defaultLayout; + } + + private void loadDefaultLayout() { + JsonNode definition = loadJsonFile(); + defaultLayout = ApplicationLayoutDto.builder().definition(definition).build(); + } + + private JsonNode loadJsonFile() { + ObjectMapper objectMapper = new ObjectMapper(); + try { + ClassPathResource resource = new ClassPathResource("defaultLayout.json"); + return objectMapper.readTree(resource.getInputStream()); + } catch (IOException e) { + logger.warn("Failed to read default application layout, returning empty node"); + return objectMapper.createObjectNode(); + } + } +} diff --git a/layout-server/src/main/resources/defaultLayout.json b/layout-server/src/main/resources/defaultLayout.json new file mode 100644 index 000000000..871b11b44 --- /dev/null +++ b/layout-server/src/main/resources/defaultLayout.json @@ -0,0 +1,22 @@ +{ + "id": "main-tabs", + "type": "Stack", + "props": { + "className": "vuuShell-mainTabs", + "TabstripProps": { + "allowAddTab": true, + "allowRenameTab": true, + "animateSelectionThumb": false, + "className": "vuuShellMainTabstrip", + "location": "main-tab" + }, + "preserve": true, + "active": 0 + }, + "children": [ + { + "type": "Placeholder", + "title": "Page 1" + } + ] +} diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java new file mode 100644 index 000000000..b3fe52f5a --- /dev/null +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java @@ -0,0 +1,145 @@ +package org.finos.vuu.layoutserver.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.finos.vuu.layoutserver.dto.ApplicationLayoutDto; +import org.finos.vuu.layoutserver.model.ApplicationLayout; +import org.finos.vuu.layoutserver.repository.ApplicationLayoutRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.dao.EmptyResultDataAccessException; + +import java.util.NoSuchElementException; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class ApplicationLayoutServiceTest { + + private static ApplicationLayoutRepository mockRepo; + private static ApplicationLayoutService service; + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private static final String defaultLayout = + "{" + + " \"id\": \"main-tabs\"," + + " \"type\": \"Stack\"," + + " \"props\": {" + + " \"className\": \"vuuShell-mainTabs\"," + + " \"TabstripProps\": {" + + " \"allowAddTab\": true," + + " \"allowRenameTab\": true," + + " \"animateSelectionThumb\": false," + + " \"className\": \"vuuShellMainTabstrip\"," + + " \"location\": \"main-tab\"" + + " }," + + " \"preserve\": true," + + " \"active\": 0" + + " }," + + " \"children\": [" + + " {" + + " \"type\": \"Placeholder\"," + + " \"title\": \"Page 1\"" + + " }" + + " ]" + + "}"; + + @BeforeEach + public void setup() { + mockRepo = Mockito.mock(ApplicationLayoutRepository.class); + service = new ApplicationLayoutService(mockRepo); + } + + @Test + public void getApplicationLayout_noLayout_returnsDefault() { + when(mockRepo.findById(anyString())).thenReturn(Optional.empty()); + + ApplicationLayoutDto actualLayout = service.getApplicationLayout("new user"); + + assertThat(actualLayout.getUser()).isNull(); + assertThat(actualLayout.getDefinition().toString()).isEqualToIgnoringWhitespace(defaultLayout); + } + + @Test + public void getApplicationLayout_layoutExists_returnsLayout() { + String expectedDefinition = "{\"id\":\"main-tabs\"}"; + String user = "user"; + ApplicationLayout expectedLayout = new ApplicationLayout(user, expectedDefinition); + + when(mockRepo.findById(user)).thenReturn(Optional.of(expectedLayout)); + + ApplicationLayoutDto actualLayout = service.getApplicationLayout(user); + + assertThat(actualLayout.getUser()).isEqualTo(user); + assertThat(actualLayout.getDefinition().toString()).isEqualToIgnoringWhitespace(expectedDefinition); + } + + @Test + public void createApplicationLayout_validDefinition_callsRepoSave() throws JsonProcessingException { + String definition = "{\"id\":\"main-tabs\"}"; + String user = "user"; + + service.createApplicationLayout(user, objectMapper.readTree(definition)); + + ApplicationLayout expectedLayout = new ApplicationLayout(user, definition); + verify(mockRepo, times(1)).save(expectedLayout); + } + + @Test + public void createApplicationLayout_invalidDefinition_throwsJsonException() { + String definition = "invalid JSON"; + + assertThrows(JsonProcessingException.class, () -> + service.createApplicationLayout("user", objectMapper.readTree(definition)) + ); + } + + @Test + public void updateApplicationLayout_validDefinition_callsRepoSave() throws JsonProcessingException { + String definition = "{\"id\":\"main-tabs\"}"; + String user = "user"; + + service.updateApplicationLayout(user, objectMapper.readTree(definition)); + + ApplicationLayout expectedLayout = new ApplicationLayout(user, definition); + verify(mockRepo, times(1)).save(expectedLayout); + } + + @Test + public void updateApplicationLayout_invalidDefinition_throwsJsonException() { + String definition = "invalid JSON"; + + assertThrows(JsonProcessingException.class, () -> + service.updateApplicationLayout("user", objectMapper.readTree(definition)) + ); + } + + @Test + public void deleteApplicationLayout_entryExists_callsRepoDelete() { + String user = "user"; + + service.deleteApplicationLayout(user); + + verify(mockRepo, times(1)).deleteById(user); + } + + @Test + public void deleteApplicationLayout_deleteFails_throwsException() { + String user = "user"; + + doThrow(EmptyResultDataAccessException.class).when(mockRepo).deleteById(user); + + NoSuchElementException exception = assertThrows(NoSuchElementException.class, () -> + service.deleteApplicationLayout(user) + ); + + assertThat(exception.getMessage()).isEqualTo("No layout found for user: " + user); + } +} From b5e0476093ea6850ba023ab81fbf4e215838b1f7 Mon Sep 17 00:00:00 2001 From: Peter Ling Date: Mon, 16 Oct 2023 15:42:29 +0100 Subject: [PATCH 11/56] VUU-70: Add integration tests --- .../ApplicationLayoutController.java | 8 +- .../{ => response}/ApplicationLayoutDto.java | 2 +- .../service/ApplicationLayoutService.java | 2 +- .../ApplicationLayoutIntegrationTest.java | 184 ++++++++++++++++++ .../service/ApplicationLayoutServiceTest.java | 2 +- .../src/test/resources/defaultLayout.json | 3 + 6 files changed, 194 insertions(+), 7 deletions(-) rename layout-server/src/main/java/org/finos/vuu/layoutserver/dto/{ => response}/ApplicationLayoutDto.java (94%) create mode 100644 layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java create mode 100644 layout-server/src/test/resources/defaultLayout.json diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java index 540234630..95d3d2637 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java @@ -2,7 +2,7 @@ import com.fasterxml.jackson.databind.JsonNode; import lombok.RequiredArgsConstructor; -import org.finos.vuu.layoutserver.dto.ApplicationLayoutDto; +import org.finos.vuu.layoutserver.dto.response.ApplicationLayoutDto; import org.finos.vuu.layoutserver.service.ApplicationLayoutService; import org.springframework.http.HttpStatus; import org.springframework.validation.annotation.Validated; @@ -42,7 +42,7 @@ public ApplicationLayoutDto getApplicationLayout(@RequestHeader("user") String u */ @ResponseStatus(HttpStatus.CREATED) @PostMapping - public void createLayout(@RequestHeader("user") String username, @RequestBody JsonNode layoutDefinition) { + public void createApplicationLayout(@RequestHeader("user") String username, @RequestBody JsonNode layoutDefinition) { service.createApplicationLayout(username, layoutDefinition); } @@ -54,7 +54,7 @@ public void createLayout(@RequestHeader("user") String username, @RequestBody Js */ @ResponseStatus(HttpStatus.NO_CONTENT) @PutMapping - public void updateLayout(@RequestHeader("user") String username, @RequestBody JsonNode layoutDefinition) { + public void updateApplicationLayout(@RequestHeader("user") String username, @RequestBody JsonNode layoutDefinition) { service.updateApplicationLayout(username, layoutDefinition); } @@ -65,7 +65,7 @@ public void updateLayout(@RequestHeader("user") String username, @RequestBody Js */ @ResponseStatus(HttpStatus.NO_CONTENT) @DeleteMapping - public void deleteLayout(@RequestHeader("user") String username) { + public void deleteApplicationLayout(@RequestHeader("user") String username) { service.deleteApplicationLayout(username); } } diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/ApplicationLayoutDto.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/ApplicationLayoutDto.java similarity index 94% rename from layout-server/src/main/java/org/finos/vuu/layoutserver/dto/ApplicationLayoutDto.java rename to layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/ApplicationLayoutDto.java index 52e9455f1..929781ee4 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/ApplicationLayoutDto.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/ApplicationLayoutDto.java @@ -1,4 +1,4 @@ -package org.finos.vuu.layoutserver.dto; +package org.finos.vuu.layoutserver.dto.response; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java index f20833a32..d76e61806 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java @@ -4,7 +4,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; -import org.finos.vuu.layoutserver.dto.ApplicationLayoutDto; +import org.finos.vuu.layoutserver.dto.response.ApplicationLayoutDto; import org.finos.vuu.layoutserver.model.ApplicationLayout; import org.finos.vuu.layoutserver.repository.ApplicationLayoutRepository; import org.slf4j.Logger; diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java new file mode 100644 index 000000000..9143649cb --- /dev/null +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java @@ -0,0 +1,184 @@ +package org.finos.vuu.layoutserver.integration; + +import org.finos.vuu.layoutserver.model.ApplicationLayout; +import org.finos.vuu.layoutserver.repository.ApplicationLayoutRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +public class ApplicationLayoutIntegrationTest { + @Autowired + private MockMvc mockMvc; + @Autowired + private ApplicationLayoutRepository repository; + + @Test + public void getApplicationLayout_noLayoutExists_returns200WithDefaultLayout() throws Exception { + mockMvc.perform(get("/application-layouts").header("user", "new user")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.user", nullValue())) + // Expecting application layout as defined in /test/resources/defaultLayout.json + .andExpect(jsonPath("$.definition.defaultLayoutKey", is("default-layout-value"))); + } + + @Test + public void getApplicationLayout_layoutExists_returns200WithPersistedLayout() throws Exception { + String user = "user"; + + Map definition = new HashMap<>(); + definition.put("defKey", "defVal"); + + persistApplicationLayout(user, definition); + + mockMvc.perform(get("/application-layouts").header("user", user)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.user", is(user))) + .andExpect(jsonPath("$.definition", is(definition))); + } + + @Test + public void createApplicationLayout_noLayoutExists_returns201AndPersistsLayout() throws Exception { + String user = "user"; + String definition = "{\"key\":\"value\"}"; + + mockMvc.perform(post("/application-layouts") + .header("user", user) + .content(definition) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$").doesNotExist()); + + ApplicationLayout persistedLayout = repository.findById(user).orElseThrow(); + + assertThat(persistedLayout.getUsername()).isEqualTo(user); + assertThat(persistedLayout.extractDefinition()).isEqualTo(definition); + } + + @Test + public void createApplicationLayout_layoutExists_returns201AndOverwritesLayout() throws Exception { + String user = "user"; + + Map initialDefinition = new HashMap<>(); + initialDefinition.put("initial-key", "initial-value"); + + persistApplicationLayout(user, initialDefinition); + + String newDefinition = "{\"new-key\":\"new-value\"}"; + + mockMvc.perform(post("/application-layouts") + .header("user", user) + .content(newDefinition) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$").doesNotExist()); + + assertThat(repository.findAll()).hasSize(1); + + ApplicationLayout retrievedLayout = repository.findById(user).orElseThrow(); + + assertThat(retrievedLayout.getUsername()).isEqualTo(user); + assertThat(retrievedLayout.extractDefinition()).isEqualTo(newDefinition); + } + + @Test + public void updateApplicationLayout_noLayoutExists_returns204AndPersistsLayout() throws Exception { + String user = "user"; + String definition = "{\"key\":\"value\"}"; + + mockMvc.perform(put("/application-layouts") + .header("user", user) + .content(definition) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()) + .andExpect(jsonPath("$").doesNotExist()); + + ApplicationLayout persistedLayout = repository.findById(user).orElseThrow(); + + assertThat(persistedLayout.getUsername()).isEqualTo(user); + assertThat(persistedLayout.extractDefinition()).isEqualTo(definition); + } + + @Test + public void updateApplicationLayout_layoutExists_returns204AndOverwritesLayout() throws Exception { + String user = "user"; + + Map initialDefinition = new HashMap<>(); + initialDefinition.put("initial-key", "initial-value"); + + persistApplicationLayout(user, initialDefinition); + + String newDefinition = "{\"new-key\":\"new-value\"}"; + + mockMvc.perform(put("/application-layouts") + .header("user", user) + .content(newDefinition) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()) + .andExpect(jsonPath("$").doesNotExist()); + + assertThat(repository.findAll()).hasSize(1); + + ApplicationLayout retrievedLayout = repository.findById(user).orElseThrow(); + + assertThat(retrievedLayout.getUsername()).isEqualTo(user); + assertThat(retrievedLayout.extractDefinition()).isEqualTo(newDefinition); + } + + @Test + public void deleteApplicationLayout_noLayoutExists_returns404() throws Exception { + String user = "user"; + + String response = mockMvc.perform(delete("/application-layouts") + .header("user", user)) + .andExpect(status().isNotFound()) + .andReturn().getResponse().getContentAsString(); + + assertThat(response).isEqualTo("No layout found for user: " + user); + } + + @Test + public void deleteApplicationLayout_layoutExists_returns204AndDeletesLayout() throws Exception { + String user = "user"; + + Map initialDefinition = new HashMap<>(); + initialDefinition.put("initial-key", "initial-value"); + + persistApplicationLayout(user, initialDefinition); + + mockMvc.perform(delete("/application-layouts") + .header("user", user)) + .andExpect(status().isNoContent()) + .andExpect(jsonPath("$").doesNotExist()); + + assertThat(repository.findAll()).hasSize(0); + } + + private void persistApplicationLayout(String user, Map definition) { + StringBuilder defBuilder = new StringBuilder("{"); + definition.forEach((k, v) -> defBuilder.append("\"").append(k).append("\":\"").append(v).append("\"")); + defBuilder.append("}"); + + ApplicationLayout appLayout = new ApplicationLayout(user, defBuilder.toString()); + repository.save(appLayout); + } +} diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java index b3fe52f5a..3dbc39706 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java @@ -2,7 +2,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import org.finos.vuu.layoutserver.dto.ApplicationLayoutDto; +import org.finos.vuu.layoutserver.dto.response.ApplicationLayoutDto; import org.finos.vuu.layoutserver.model.ApplicationLayout; import org.finos.vuu.layoutserver.repository.ApplicationLayoutRepository; import org.junit.jupiter.api.BeforeEach; diff --git a/layout-server/src/test/resources/defaultLayout.json b/layout-server/src/test/resources/defaultLayout.json new file mode 100644 index 000000000..87a79e544 --- /dev/null +++ b/layout-server/src/test/resources/defaultLayout.json @@ -0,0 +1,3 @@ +{ + "defaultLayoutKey": "default-layout-value" +} From 957148b12bfefa8ff4baf3757a2fed970675ee2c Mon Sep 17 00:00:00 2001 From: Peter Ling Date: Tue, 17 Oct 2023 09:45:54 +0100 Subject: [PATCH 12/56] VUU-70: Add unit tests for controller --- .../ApplicationLayoutControllerTest.java | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 layout-server/src/test/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutControllerTest.java diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutControllerTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutControllerTest.java new file mode 100644 index 000000000..6c51e2f2c --- /dev/null +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutControllerTest.java @@ -0,0 +1,71 @@ +package org.finos.vuu.layoutserver.controller; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.finos.vuu.layoutserver.dto.response.ApplicationLayoutDto; +import org.finos.vuu.layoutserver.service.ApplicationLayoutService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class ApplicationLayoutControllerTest { + private static ApplicationLayoutService mockService; + private static ApplicationLayoutController controller; + private static final ObjectMapper objectMapper = new ObjectMapper(); + + @BeforeEach + public void setup() { + mockService = Mockito.mock(ApplicationLayoutService.class); + controller = new ApplicationLayoutController(mockService); + } + + @Test + public void getApplicationLayout_validUser_returnsLayoutFromService() throws JsonProcessingException { + String user = "user"; + + ApplicationLayoutDto expectedDto = ApplicationLayoutDto.builder() + .user(user) + .definition(objectMapper.readTree("{\"id\":\"main-tabs\"}")) + .build(); + + when(mockService.getApplicationLayout(user)).thenReturn(expectedDto); + + assertThat(controller.getApplicationLayout(user)).isEqualTo(expectedDto); + verify(mockService, times(1)).getApplicationLayout(user); + } + + @Test + public void createApplicationLayout_validUser_callsService() throws JsonProcessingException { + String user = "user"; + JsonNode definition = objectMapper.readTree("{\"id\":\"main-tabs\"}"); + + controller.createApplicationLayout(user, definition); + + verify(mockService, times(1)).createApplicationLayout(user, definition); + } + + @Test + public void updateApplicationLayout_validUser_callsService() throws JsonProcessingException { + String user = "user"; + JsonNode definition = objectMapper.readTree("{\"id\":\"main-tabs\"}"); + + controller.updateApplicationLayout(user, definition); + + verify(mockService, times(1)).updateApplicationLayout(user, definition); + } + + @Test + public void deleteApplicationLayout_validUser_callsService() { + String user = "user"; + + controller.deleteApplicationLayout(user); + + verify(mockService, times(1)).deleteApplicationLayout(user); + } +} From fe5c279aadf58e6c16582660bfb65313439ebdee Mon Sep 17 00:00:00 2001 From: Peter Ling Date: Tue, 17 Oct 2023 11:09:30 +0100 Subject: [PATCH 13/56] VUU-70: Fix service test --- .../ApplicationLayoutController.java | 1 + .../service/ApplicationLayoutServiceTest.java | 31 +++---------------- 2 files changed, 6 insertions(+), 26 deletions(-) diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java index 95d3d2637..b7095db61 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java @@ -29,6 +29,7 @@ public class ApplicationLayoutController { * * @return the application layout */ + @ResponseStatus(HttpStatus.OK) @GetMapping public ApplicationLayoutDto getApplicationLayout(@RequestHeader("user") String username) { return service.getApplicationLayout(username); diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java index 3dbc39706..8dd7d1626 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java @@ -1,6 +1,7 @@ package org.finos.vuu.layoutserver.service; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.finos.vuu.layoutserver.dto.response.ApplicationLayoutDto; import org.finos.vuu.layoutserver.model.ApplicationLayout; @@ -27,30 +28,6 @@ class ApplicationLayoutServiceTest { private static ApplicationLayoutService service; private static final ObjectMapper objectMapper = new ObjectMapper(); - private static final String defaultLayout = - "{" + - " \"id\": \"main-tabs\"," + - " \"type\": \"Stack\"," + - " \"props\": {" + - " \"className\": \"vuuShell-mainTabs\"," + - " \"TabstripProps\": {" + - " \"allowAddTab\": true," + - " \"allowRenameTab\": true," + - " \"animateSelectionThumb\": false," + - " \"className\": \"vuuShellMainTabstrip\"," + - " \"location\": \"main-tab\"" + - " }," + - " \"preserve\": true," + - " \"active\": 0" + - " }," + - " \"children\": [" + - " {" + - " \"type\": \"Placeholder\"," + - " \"title\": \"Page 1\"" + - " }" + - " ]" + - "}"; - @BeforeEach public void setup() { mockRepo = Mockito.mock(ApplicationLayoutRepository.class); @@ -58,13 +35,15 @@ public void setup() { } @Test - public void getApplicationLayout_noLayout_returnsDefault() { + public void getApplicationLayout_noLayout_returnsDefault() throws JsonProcessingException { when(mockRepo.findById(anyString())).thenReturn(Optional.empty()); ApplicationLayoutDto actualLayout = service.getApplicationLayout("new user"); + // Expecting application layout as defined in /test/resources/defaultLayout.json + JsonNode expectedDefinition = objectMapper.readTree("{\"defaultLayoutKey\":\"default-layout-value\"}"); assertThat(actualLayout.getUser()).isNull(); - assertThat(actualLayout.getDefinition().toString()).isEqualToIgnoringWhitespace(defaultLayout); + assertThat(actualLayout.getDefinition()).isEqualTo(expectedDefinition); } @Test From fc8e069383b59b877865b5072364b9b7836e399a Mon Sep 17 00:00:00 2001 From: Peter Ling Date: Tue, 17 Oct 2023 11:25:56 +0100 Subject: [PATCH 14/56] VUU-70: Extract constant for default layout file --- .../vuu/layoutserver/service/ApplicationLayoutService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java index d76e61806..e6fdbc549 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java @@ -22,6 +22,7 @@ public class ApplicationLayoutService { private static final Logger logger = LoggerFactory.getLogger(ApplicationLayoutService.class); + private static final String DEFAULT_LAYOUT_FILE = "defaultLayout.json"; private static ApplicationLayoutDto defaultLayout; private final ApplicationLayoutRepository repository; @@ -72,7 +73,7 @@ private void loadDefaultLayout() { private JsonNode loadJsonFile() { ObjectMapper objectMapper = new ObjectMapper(); try { - ClassPathResource resource = new ClassPathResource("defaultLayout.json"); + ClassPathResource resource = new ClassPathResource(DEFAULT_LAYOUT_FILE); return objectMapper.readTree(resource.getInputStream()); } catch (IOException e) { logger.warn("Failed to read default application layout, returning empty node"); From 108b087e974ab0206dfff6e205e9b29cc09c486d Mon Sep 17 00:00:00 2001 From: Peter Ling Date: Tue, 17 Oct 2023 11:26:13 +0100 Subject: [PATCH 15/56] VUU-70: Adjust whitespace --- .../ApplicationLayoutIntegrationTest.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java index 9143649cb..2a983b156 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java @@ -59,7 +59,7 @@ public void getApplicationLayout_layoutExists_returns200WithPersistedLayout() th @Test public void createApplicationLayout_noLayoutExists_returns201AndPersistsLayout() throws Exception { String user = "user"; - String definition = "{\"key\":\"value\"}"; + String definition = "{\"key\": \"value\"}"; mockMvc.perform(post("/application-layouts") .header("user", user) @@ -71,7 +71,7 @@ public void createApplicationLayout_noLayoutExists_returns201AndPersistsLayout() ApplicationLayout persistedLayout = repository.findById(user).orElseThrow(); assertThat(persistedLayout.getUsername()).isEqualTo(user); - assertThat(persistedLayout.extractDefinition()).isEqualTo(definition); + assertThat(persistedLayout.extractDefinition()).isEqualToIgnoringWhitespace(definition); } @Test @@ -83,7 +83,7 @@ public void createApplicationLayout_layoutExists_returns201AndOverwritesLayout() persistApplicationLayout(user, initialDefinition); - String newDefinition = "{\"new-key\":\"new-value\"}"; + String newDefinition = "{\"new-key\": \"new-value\"}"; mockMvc.perform(post("/application-layouts") .header("user", user) @@ -97,13 +97,13 @@ public void createApplicationLayout_layoutExists_returns201AndOverwritesLayout() ApplicationLayout retrievedLayout = repository.findById(user).orElseThrow(); assertThat(retrievedLayout.getUsername()).isEqualTo(user); - assertThat(retrievedLayout.extractDefinition()).isEqualTo(newDefinition); + assertThat(retrievedLayout.extractDefinition()).isEqualToIgnoringWhitespace(newDefinition); } @Test public void updateApplicationLayout_noLayoutExists_returns204AndPersistsLayout() throws Exception { String user = "user"; - String definition = "{\"key\":\"value\"}"; + String definition = "{\"key\": \"value\"}"; mockMvc.perform(put("/application-layouts") .header("user", user) @@ -115,7 +115,7 @@ public void updateApplicationLayout_noLayoutExists_returns204AndPersistsLayout() ApplicationLayout persistedLayout = repository.findById(user).orElseThrow(); assertThat(persistedLayout.getUsername()).isEqualTo(user); - assertThat(persistedLayout.extractDefinition()).isEqualTo(definition); + assertThat(persistedLayout.extractDefinition()).isEqualToIgnoringWhitespace(definition); } @Test @@ -127,7 +127,7 @@ public void updateApplicationLayout_layoutExists_returns204AndOverwritesLayout() persistApplicationLayout(user, initialDefinition); - String newDefinition = "{\"new-key\":\"new-value\"}"; + String newDefinition = "{\"new-key\": \"new-value\"}"; mockMvc.perform(put("/application-layouts") .header("user", user) @@ -141,7 +141,7 @@ public void updateApplicationLayout_layoutExists_returns204AndOverwritesLayout() ApplicationLayout retrievedLayout = repository.findById(user).orElseThrow(); assertThat(retrievedLayout.getUsername()).isEqualTo(user); - assertThat(retrievedLayout.extractDefinition()).isEqualTo(newDefinition); + assertThat(retrievedLayout.extractDefinition()).isEqualToIgnoringWhitespace(newDefinition); } @Test From fe796e64432e5491459b4604417e10e8c5a49fb9 Mon Sep 17 00:00:00 2001 From: Peter Ling Date: Wed, 18 Oct 2023 11:35:05 +0100 Subject: [PATCH 16/56] VUU-70: Use AttributeConverter for JsonNode --- .../config/JsonNodeConverter.java | 43 +++++++++++++++++++ .../ApplicationLayoutController.java | 4 +- .../dto/response/ApplicationLayoutDto.java | 11 +---- .../layoutserver/model/ApplicationLayout.java | 16 +++---- .../service/ApplicationLayoutService.java | 19 +++----- .../ApplicationLayoutControllerTest.java | 16 ++++--- .../ApplicationLayoutIntegrationTest.java | 22 +++++----- .../service/ApplicationLayoutServiceTest.java | 32 +++++++------- 8 files changed, 93 insertions(+), 70 deletions(-) create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/config/JsonNodeConverter.java diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/config/JsonNodeConverter.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/config/JsonNodeConverter.java new file mode 100644 index 000000000..728e7635a --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/config/JsonNodeConverter.java @@ -0,0 +1,43 @@ +package org.finos.vuu.layoutserver.config; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.persistence.AttributeConverter; +import java.io.IOException; + +public class JsonNodeConverter implements AttributeConverter { + private static final Logger logger = LoggerFactory.getLogger(JsonNodeConverter.class); + private static final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public String convertToDatabaseColumn(JsonNode definition) { + try { + return objectMapper.writeValueAsString(definition); + } catch (final JsonProcessingException e) { + logger.error("JSON writing error", e); + return null; + } + } + + @Override + public JsonNode convertToEntityAttribute(String definition) { + try { + return objectMapper.readValue(extractDefinition(definition), new TypeReference<>() {}); + } catch (final IOException e) { + logger.error("JSON reading error", e); + return null; + } + } + + private String extractDefinition(String definition) { + if (definition.startsWith("\"") && definition.endsWith("\"")) { + definition = definition.substring(1, definition.length() - 1); + } + return definition.replaceAll("\\\\", ""); + } +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java index b7095db61..dae53281d 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java @@ -4,6 +4,7 @@ import lombok.RequiredArgsConstructor; import org.finos.vuu.layoutserver.dto.response.ApplicationLayoutDto; import org.finos.vuu.layoutserver.service.ApplicationLayoutService; +import org.modelmapper.ModelMapper; import org.springframework.http.HttpStatus; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.DeleteMapping; @@ -23,6 +24,7 @@ public class ApplicationLayoutController { private final ApplicationLayoutService service; + private final ModelMapper mapper; /** * Gets the application layout for the requesting user. Returns a default layout if none exists. @@ -32,7 +34,7 @@ public class ApplicationLayoutController { @ResponseStatus(HttpStatus.OK) @GetMapping public ApplicationLayoutDto getApplicationLayout(@RequestHeader("user") String username) { - return service.getApplicationLayout(username); + return mapper.map(service.getApplicationLayout(username), ApplicationLayoutDto.class); } /** diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/ApplicationLayoutDto.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/ApplicationLayoutDto.java index 929781ee4..7d271959f 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/ApplicationLayoutDto.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/ApplicationLayoutDto.java @@ -1,25 +1,16 @@ package org.finos.vuu.layoutserver.dto.response; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.RequiredArgsConstructor; -import org.finos.vuu.layoutserver.model.ApplicationLayout; @Data @RequiredArgsConstructor @AllArgsConstructor @Builder public class ApplicationLayoutDto { - private String user; + private String username; private JsonNode definition; - - public static ApplicationLayoutDto fromEntity(ApplicationLayout entity) throws JsonProcessingException { - ObjectMapper objectMapper = new ObjectMapper(); - JsonNode definition = objectMapper.readTree(entity.extractDefinition()); - return new ApplicationLayoutDto(entity.getUsername(), definition); - } } diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/ApplicationLayout.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/ApplicationLayout.java index 192680359..7c69de7a7 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/ApplicationLayout.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/ApplicationLayout.java @@ -1,10 +1,13 @@ package org.finos.vuu.layoutserver.model; +import com.fasterxml.jackson.databind.JsonNode; import lombok.AllArgsConstructor; import lombok.Data; import lombok.RequiredArgsConstructor; +import org.finos.vuu.layoutserver.config.JsonNodeConverter; import javax.persistence.Column; +import javax.persistence.Convert; import javax.persistence.Entity; import javax.persistence.Id; @@ -16,16 +19,7 @@ public class ApplicationLayout { @Id private String username; + @Convert(converter = JsonNodeConverter.class) @Column(columnDefinition = "JSON") - private String definition; - - public String extractDefinition() { - String extractedDefinition = definition; - - if (extractedDefinition.startsWith("\"") && extractedDefinition.endsWith("\"")) { - extractedDefinition = extractedDefinition.substring(1, extractedDefinition.length() - 1); - } - - return extractedDefinition.replaceAll("\\\\", ""); - } + private JsonNode definition; } diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java index e6fdbc549..f141cc88a 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java @@ -1,10 +1,8 @@ package org.finos.vuu.layoutserver.service; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; -import org.finos.vuu.layoutserver.dto.response.ApplicationLayoutDto; import org.finos.vuu.layoutserver.model.ApplicationLayout; import org.finos.vuu.layoutserver.repository.ApplicationLayoutRepository; import org.slf4j.Logger; @@ -23,14 +21,14 @@ public class ApplicationLayoutService { private static final Logger logger = LoggerFactory.getLogger(ApplicationLayoutService.class); private static final String DEFAULT_LAYOUT_FILE = "defaultLayout.json"; - private static ApplicationLayoutDto defaultLayout; + private static ApplicationLayout defaultLayout; private final ApplicationLayoutRepository repository; public void createApplicationLayout(String username, JsonNode layoutDefinition) { - repository.save(new ApplicationLayout(username, layoutDefinition.toString())); + repository.save(new ApplicationLayout(username, layoutDefinition)); } - public ApplicationLayoutDto getApplicationLayout(String username) { + public ApplicationLayout getApplicationLayout(String username) { Optional layout = repository.findById(username); if (layout.isEmpty()) { @@ -38,12 +36,7 @@ public ApplicationLayoutDto getApplicationLayout(String username) { return getDefaultLayout(); } - try { - return ApplicationLayoutDto.fromEntity(layout.get()); - } catch (JsonProcessingException e) { - logger.warn("Failed to read user's application layout, returning default"); - return getDefaultLayout(); - } + return layout.get(); } public void updateApplicationLayout(String username, JsonNode layoutDefinition) { @@ -58,7 +51,7 @@ public void deleteApplicationLayout(String username) { } } - private ApplicationLayoutDto getDefaultLayout() { + private ApplicationLayout getDefaultLayout() { if (defaultLayout == null) { loadDefaultLayout(); } @@ -67,7 +60,7 @@ private ApplicationLayoutDto getDefaultLayout() { private void loadDefaultLayout() { JsonNode definition = loadJsonFile(); - defaultLayout = ApplicationLayoutDto.builder().definition(definition).build(); + defaultLayout = new ApplicationLayout(null, definition); } private JsonNode loadJsonFile() { diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutControllerTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutControllerTest.java index 6c51e2f2c..2fba75110 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutControllerTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutControllerTest.java @@ -4,10 +4,12 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.finos.vuu.layoutserver.dto.response.ApplicationLayoutDto; +import org.finos.vuu.layoutserver.model.ApplicationLayout; import org.finos.vuu.layoutserver.service.ApplicationLayoutService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; +import org.modelmapper.ModelMapper; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.times; @@ -17,26 +19,26 @@ class ApplicationLayoutControllerTest { private static ApplicationLayoutService mockService; private static ApplicationLayoutController controller; + private static final ModelMapper modelMapper = new ModelMapper(); private static final ObjectMapper objectMapper = new ObjectMapper(); @BeforeEach public void setup() { mockService = Mockito.mock(ApplicationLayoutService.class); - controller = new ApplicationLayoutController(mockService); + controller = new ApplicationLayoutController(mockService, modelMapper); } @Test public void getApplicationLayout_validUser_returnsLayoutFromService() throws JsonProcessingException { String user = "user"; + JsonNode definition = objectMapper.readTree("{\"id\":\"main-tabs\"}"); - ApplicationLayoutDto expectedDto = ApplicationLayoutDto.builder() - .user(user) - .definition(objectMapper.readTree("{\"id\":\"main-tabs\"}")) - .build(); + when(mockService.getApplicationLayout(user)) + .thenReturn(new ApplicationLayout(user, definition)); - when(mockService.getApplicationLayout(user)).thenReturn(expectedDto); + assertThat(controller.getApplicationLayout(user)) + .isEqualTo(new ApplicationLayoutDto(user, definition)); - assertThat(controller.getApplicationLayout(user)).isEqualTo(expectedDto); verify(mockService, times(1)).getApplicationLayout(user); } diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java index 2a983b156..a0c5837f2 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java @@ -1,5 +1,7 @@ package org.finos.vuu.layoutserver.integration; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import org.finos.vuu.layoutserver.model.ApplicationLayout; import org.finos.vuu.layoutserver.repository.ApplicationLayoutRepository; import org.junit.jupiter.api.Test; @@ -27,6 +29,7 @@ @AutoConfigureMockMvc @ActiveProfiles("test") public class ApplicationLayoutIntegrationTest { + public static final ObjectMapper objectMapper = new ObjectMapper(); @Autowired private MockMvc mockMvc; @Autowired @@ -36,7 +39,7 @@ public class ApplicationLayoutIntegrationTest { public void getApplicationLayout_noLayoutExists_returns200WithDefaultLayout() throws Exception { mockMvc.perform(get("/application-layouts").header("user", "new user")) .andExpect(status().isOk()) - .andExpect(jsonPath("$.user", nullValue())) + .andExpect(jsonPath("$.username", nullValue())) // Expecting application layout as defined in /test/resources/defaultLayout.json .andExpect(jsonPath("$.definition.defaultLayoutKey", is("default-layout-value"))); } @@ -52,7 +55,7 @@ public void getApplicationLayout_layoutExists_returns200WithPersistedLayout() th mockMvc.perform(get("/application-layouts").header("user", user)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.user", is(user))) + .andExpect(jsonPath("$.username", is(user))) .andExpect(jsonPath("$.definition", is(definition))); } @@ -71,7 +74,7 @@ public void createApplicationLayout_noLayoutExists_returns201AndPersistsLayout() ApplicationLayout persistedLayout = repository.findById(user).orElseThrow(); assertThat(persistedLayout.getUsername()).isEqualTo(user); - assertThat(persistedLayout.extractDefinition()).isEqualToIgnoringWhitespace(definition); + assertThat(persistedLayout.getDefinition()).isEqualTo(objectMapper.readTree(definition)); } @Test @@ -97,7 +100,7 @@ public void createApplicationLayout_layoutExists_returns201AndOverwritesLayout() ApplicationLayout retrievedLayout = repository.findById(user).orElseThrow(); assertThat(retrievedLayout.getUsername()).isEqualTo(user); - assertThat(retrievedLayout.extractDefinition()).isEqualToIgnoringWhitespace(newDefinition); + assertThat(retrievedLayout.getDefinition()).isEqualTo(objectMapper.readTree(newDefinition)); } @Test @@ -115,7 +118,7 @@ public void updateApplicationLayout_noLayoutExists_returns204AndPersistsLayout() ApplicationLayout persistedLayout = repository.findById(user).orElseThrow(); assertThat(persistedLayout.getUsername()).isEqualTo(user); - assertThat(persistedLayout.extractDefinition()).isEqualToIgnoringWhitespace(definition); + assertThat(persistedLayout.getDefinition()).isEqualTo(objectMapper.readTree(definition)); } @Test @@ -141,7 +144,7 @@ public void updateApplicationLayout_layoutExists_returns204AndOverwritesLayout() ApplicationLayout retrievedLayout = repository.findById(user).orElseThrow(); assertThat(retrievedLayout.getUsername()).isEqualTo(user); - assertThat(retrievedLayout.extractDefinition()).isEqualToIgnoringWhitespace(newDefinition); + assertThat(retrievedLayout.getDefinition()).isEqualTo(objectMapper.readTree(newDefinition)); } @Test @@ -174,11 +177,6 @@ public void deleteApplicationLayout_layoutExists_returns204AndDeletesLayout() th } private void persistApplicationLayout(String user, Map definition) { - StringBuilder defBuilder = new StringBuilder("{"); - definition.forEach((k, v) -> defBuilder.append("\"").append(k).append("\":\"").append(v).append("\"")); - defBuilder.append("}"); - - ApplicationLayout appLayout = new ApplicationLayout(user, defBuilder.toString()); - repository.save(appLayout); + repository.save(new ApplicationLayout(user, objectMapper.convertValue(definition, JsonNode.class))); } } diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java index 8dd7d1626..297b917e4 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java @@ -3,7 +3,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import org.finos.vuu.layoutserver.dto.response.ApplicationLayoutDto; import org.finos.vuu.layoutserver.model.ApplicationLayout; import org.finos.vuu.layoutserver.repository.ApplicationLayoutRepository; import org.junit.jupiter.api.BeforeEach; @@ -38,37 +37,38 @@ public void setup() { public void getApplicationLayout_noLayout_returnsDefault() throws JsonProcessingException { when(mockRepo.findById(anyString())).thenReturn(Optional.empty()); - ApplicationLayoutDto actualLayout = service.getApplicationLayout("new user"); + ApplicationLayout actualLayout = service.getApplicationLayout("new user"); + // Expecting application layout as defined in /test/resources/defaultLayout.json JsonNode expectedDefinition = objectMapper.readTree("{\"defaultLayoutKey\":\"default-layout-value\"}"); - assertThat(actualLayout.getUser()).isNull(); + assertThat(actualLayout.getUsername()).isNull(); assertThat(actualLayout.getDefinition()).isEqualTo(expectedDefinition); } @Test - public void getApplicationLayout_layoutExists_returnsLayout() { - String expectedDefinition = "{\"id\":\"main-tabs\"}"; + public void getApplicationLayout_layoutExists_returnsLayout() throws JsonProcessingException { String user = "user"; + + JsonNode expectedDefinition = objectMapper.readTree("{\"id\":\"main-tabs\"}"); ApplicationLayout expectedLayout = new ApplicationLayout(user, expectedDefinition); when(mockRepo.findById(user)).thenReturn(Optional.of(expectedLayout)); - ApplicationLayoutDto actualLayout = service.getApplicationLayout(user); + ApplicationLayout actualLayout = service.getApplicationLayout(user); - assertThat(actualLayout.getUser()).isEqualTo(user); - assertThat(actualLayout.getDefinition().toString()).isEqualToIgnoringWhitespace(expectedDefinition); + assertThat(actualLayout).isEqualTo(expectedLayout); } @Test public void createApplicationLayout_validDefinition_callsRepoSave() throws JsonProcessingException { - String definition = "{\"id\":\"main-tabs\"}"; String user = "user"; + JsonNode definition = objectMapper.readTree("{\"id\":\"main-tabs\"}"); - service.createApplicationLayout(user, objectMapper.readTree(definition)); + service.createApplicationLayout(user, definition); - ApplicationLayout expectedLayout = new ApplicationLayout(user, definition); - verify(mockRepo, times(1)).save(expectedLayout); + verify(mockRepo, times(1)) + .save(new ApplicationLayout(user, definition)); } @Test @@ -82,13 +82,13 @@ public void createApplicationLayout_invalidDefinition_throwsJsonException() { @Test public void updateApplicationLayout_validDefinition_callsRepoSave() throws JsonProcessingException { - String definition = "{\"id\":\"main-tabs\"}"; String user = "user"; + JsonNode definition = objectMapper.readTree("{\"id\":\"main-tabs\"}"); - service.updateApplicationLayout(user, objectMapper.readTree(definition)); + service.updateApplicationLayout(user, definition); - ApplicationLayout expectedLayout = new ApplicationLayout(user, definition); - verify(mockRepo, times(1)).save(expectedLayout); + verify(mockRepo, times(1)) + .save(new ApplicationLayout(user, definition)); } @Test From c231404038f2bd87cecb9152a8d9e79ca000c580 Mon Sep 17 00:00:00 2001 From: Peter Ling Date: Thu, 19 Oct 2023 14:46:56 +0100 Subject: [PATCH 17/56] VUU-70: Remove DTO annotations --- .../vuu/layoutserver/dto/response/ApplicationLayoutDto.java | 6 ------ .../controller/ApplicationLayoutControllerTest.java | 6 ++++-- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/ApplicationLayoutDto.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/ApplicationLayoutDto.java index 7d271959f..2b34c6151 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/ApplicationLayoutDto.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/ApplicationLayoutDto.java @@ -1,15 +1,9 @@ package org.finos.vuu.layoutserver.dto.response; import com.fasterxml.jackson.databind.JsonNode; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Data; -import lombok.RequiredArgsConstructor; @Data -@RequiredArgsConstructor -@AllArgsConstructor -@Builder public class ApplicationLayoutDto { private String username; private JsonNode definition; diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutControllerTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutControllerTest.java index 2fba75110..2209f2a3a 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutControllerTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutControllerTest.java @@ -36,8 +36,10 @@ public void getApplicationLayout_validUser_returnsLayoutFromService() throws Jso when(mockService.getApplicationLayout(user)) .thenReturn(new ApplicationLayout(user, definition)); - assertThat(controller.getApplicationLayout(user)) - .isEqualTo(new ApplicationLayoutDto(user, definition)); + ApplicationLayoutDto response = controller.getApplicationLayout(user); + + assertThat(response.getUsername()).isEqualTo(user); + assertThat(response.getDefinition()).isEqualTo(definition); verify(mockService, times(1)).getApplicationLayout(user); } From cc13be9d167dc8dd9bb745825a146c3315bcc7e5 Mon Sep 17 00:00:00 2001 From: Peter Ling Date: Thu, 19 Oct 2023 14:50:51 +0100 Subject: [PATCH 18/56] VUU-70: Rename method --- .../vuu/layoutserver/service/ApplicationLayoutService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java index f141cc88a..58bffb111 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java @@ -59,11 +59,11 @@ private ApplicationLayout getDefaultLayout() { } private void loadDefaultLayout() { - JsonNode definition = loadJsonFile(); + JsonNode definition = loadDefaultLayoutJsonFile(); defaultLayout = new ApplicationLayout(null, definition); } - private JsonNode loadJsonFile() { + private JsonNode loadDefaultLayoutJsonFile() { ObjectMapper objectMapper = new ObjectMapper(); try { ClassPathResource resource = new ClassPathResource(DEFAULT_LAYOUT_FILE); From 306dbec1d11aef09635528334370a82c0c0e6cca Mon Sep 17 00:00:00 2001 From: Peter Ling Date: Fri, 20 Oct 2023 09:30:54 +0100 Subject: [PATCH 19/56] VUU-70: Refactor getApplicationLayout --- .../layoutserver/service/ApplicationLayoutService.java | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java index 58bffb111..8f019f0cb 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java @@ -13,7 +13,6 @@ import java.io.IOException; import java.util.NoSuchElementException; -import java.util.Optional; @RequiredArgsConstructor @Service @@ -29,14 +28,10 @@ public void createApplicationLayout(String username, JsonNode layoutDefinition) } public ApplicationLayout getApplicationLayout(String username) { - Optional layout = repository.findById(username); - - if (layout.isEmpty()) { + return repository.findById(username).orElseGet(() -> { logger.info("No application layout for user, returning default"); return getDefaultLayout(); - } - - return layout.get(); + }); } public void updateApplicationLayout(String username, JsonNode layoutDefinition) { From c547efad380a3ed1c0478f670a5c538121c951e7 Mon Sep 17 00:00:00 2001 From: Peter Ling Date: Fri, 20 Oct 2023 14:58:38 +0100 Subject: [PATCH 20/56] VUU-70: Standardise DTO casing --- .../layoutserver/config/MappingConfig.java | 8 +-- .../controller/LayoutController.java | 29 ++++++----- .../{MetadataDTO.java => MetadataDto.java} | 2 +- ...tRequestDTO.java => LayoutRequestDto.java} | 7 +-- ...equestDTO.java => MetadataRequestDto.java} | 7 +-- ...esponseDTO.java => LayoutResponseDto.java} | 4 +- ...ponseDTO.java => MetadataResponseDto.java} | 7 +-- .../controller/LayoutControllerTest.java | 51 ++++++++++--------- .../integration/LayoutIntegrationTest.java | 49 +++++++++--------- 9 files changed, 85 insertions(+), 79 deletions(-) rename layout-server/src/main/java/org/finos/vuu/layoutserver/dto/{MetadataDTO.java => MetadataDto.java} (90%) rename layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/{LayoutRequestDTO.java => LayoutRequestDto.java} (89%) rename layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/{MetadataRequestDTO.java => MetadataRequestDto.java} (78%) rename layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/{LayoutResponseDTO.java => LayoutResponseDto.java} (78%) rename layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/{MetadataResponseDTO.java => MetadataResponseDto.java} (73%) diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/config/MappingConfig.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/config/MappingConfig.java index fad55680c..5b3482f85 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/config/MappingConfig.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/config/MappingConfig.java @@ -1,7 +1,7 @@ package org.finos.vuu.layoutserver.config; -import org.finos.vuu.layoutserver.dto.request.LayoutRequestDTO; -import org.finos.vuu.layoutserver.dto.request.MetadataRequestDTO; +import org.finos.vuu.layoutserver.dto.request.LayoutRequestDto; +import org.finos.vuu.layoutserver.dto.request.MetadataRequestDto; import org.finos.vuu.layoutserver.model.Layout; import org.finos.vuu.layoutserver.model.Metadata; import org.modelmapper.ModelMapper; @@ -15,10 +15,10 @@ public class MappingConfig { public ModelMapper modelMapper() { ModelMapper mapper = new ModelMapper(); - mapper.typeMap(LayoutRequestDTO.class, Layout.class) + mapper.typeMap(LayoutRequestDto.class, Layout.class) .addMappings(m -> m.skip(Layout::setId)); - mapper.typeMap(MetadataRequestDTO.class, Metadata.class) + mapper.typeMap(MetadataRequestDto.class, Metadata.class) .addMappings(m -> m.skip(Metadata::setId)); return mapper; diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java index aeaa409da..4430687df 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java @@ -1,12 +1,9 @@ package org.finos.vuu.layoutserver.controller; -import java.util.List; -import java.util.UUID; -import javax.validation.Valid; import lombok.RequiredArgsConstructor; -import org.finos.vuu.layoutserver.dto.request.LayoutRequestDTO; -import org.finos.vuu.layoutserver.dto.response.LayoutResponseDTO; -import org.finos.vuu.layoutserver.dto.response.MetadataResponseDTO; +import org.finos.vuu.layoutserver.dto.request.LayoutRequestDto; +import org.finos.vuu.layoutserver.dto.response.LayoutResponseDto; +import org.finos.vuu.layoutserver.dto.response.MetadataResponseDto; import org.finos.vuu.layoutserver.model.Layout; import org.finos.vuu.layoutserver.service.LayoutService; import org.modelmapper.ModelMapper; @@ -22,6 +19,10 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; +import javax.validation.Valid; +import java.util.List; +import java.util.UUID; + @RequiredArgsConstructor @RestController @RequestMapping("/layouts") @@ -38,8 +39,8 @@ public class LayoutController { * @return the layout */ @GetMapping("/{id}") - public LayoutResponseDTO getLayout(@PathVariable UUID id) { - return mapper.map(layoutService.getLayout(id), LayoutResponseDTO.class); + public LayoutResponseDto getLayout(@PathVariable UUID id) { + return mapper.map(layoutService.getLayout(id), LayoutResponseDto.class); } /** @@ -48,11 +49,11 @@ public LayoutResponseDTO getLayout(@PathVariable UUID id) { * @return the metadata */ @GetMapping("/metadata") - public List getMetadata() { + public List getMetadata() { return layoutService.getMetadata() .stream() - .map(metadata -> mapper.map(metadata, MetadataResponseDTO.class)) + .map(metadata -> mapper.map(metadata, MetadataResponseDto.class)) .collect(java.util.stream.Collectors.toList()); } @@ -64,13 +65,13 @@ public List getMetadata() { */ @ResponseStatus(HttpStatus.CREATED) @PostMapping - public LayoutResponseDTO createLayout( - @RequestBody @Valid LayoutRequestDTO layoutToCreate) { + public LayoutResponseDto createLayout( + @RequestBody @Valid LayoutRequestDto layoutToCreate) { Layout layout = mapper.map(layoutToCreate, Layout.class); Layout createdLayout = layoutService.getLayout(layoutService.createLayout(layout)); - return mapper.map(createdLayout, LayoutResponseDTO.class); + return mapper.map(createdLayout, LayoutResponseDto.class); } /** @@ -82,7 +83,7 @@ public LayoutResponseDTO createLayout( @ResponseStatus(HttpStatus.NO_CONTENT) @PutMapping("/{id}") public void updateLayout(@PathVariable UUID id, - @RequestBody @Valid LayoutRequestDTO layout) { + @RequestBody @Valid LayoutRequestDto layout) { Layout newLayout = mapper.map(layout, Layout.class); layoutService.updateLayout(id, newLayout); diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/MetadataDTO.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/MetadataDto.java similarity index 90% rename from layout-server/src/main/java/org/finos/vuu/layoutserver/dto/MetadataDTO.java rename to layout-server/src/main/java/org/finos/vuu/layoutserver/dto/MetadataDto.java index 23f82a691..f3f2d7b1a 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/MetadataDTO.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/MetadataDto.java @@ -1,6 +1,6 @@ package org.finos.vuu.layoutserver.dto; -public interface MetadataDTO { +public interface MetadataDto { String getName(); diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDTO.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDto.java similarity index 89% rename from layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDTO.java rename to layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDto.java index 8b93e6343..4ad9c64fd 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDTO.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDto.java @@ -1,12 +1,13 @@ package org.finos.vuu.layoutserver.dto.request; import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; -import lombok.Data; @Data -public class LayoutRequestDTO { +public class LayoutRequestDto { /** * The definition of the layout as a string (e.g. stringified JSON structure containing @@ -18,5 +19,5 @@ public class LayoutRequestDTO { @JsonProperty(value = "metadata", required = true) @NotNull(message = "Please provide valid metadata") - private MetadataRequestDTO metadata; + private MetadataRequestDto metadata; } diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDTO.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDto.java similarity index 78% rename from layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDTO.java rename to layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDto.java index ee01f1f3c..d0d0b4292 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDTO.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDto.java @@ -1,12 +1,13 @@ package org.finos.vuu.layoutserver.dto.request; import com.fasterxml.jackson.annotation.JsonProperty; -import javax.validation.constraints.NotNull; import lombok.Data; -import org.finos.vuu.layoutserver.dto.MetadataDTO; +import org.finos.vuu.layoutserver.dto.MetadataDto; + +import javax.validation.constraints.NotNull; @Data -public class MetadataRequestDTO implements MetadataDTO { +public class MetadataRequestDto implements MetadataDto { @JsonProperty(value = "name", required = true) @NotNull(message = "Please provide a valid name") diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutResponseDTO.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutResponseDto.java similarity index 78% rename from layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutResponseDTO.java rename to layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutResponseDto.java index 9e1077063..0a7f01fe0 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutResponseDTO.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutResponseDto.java @@ -5,7 +5,7 @@ import java.util.UUID; @Data -public class LayoutResponseDTO { +public class LayoutResponseDto { private UUID id; @@ -14,5 +14,5 @@ public class LayoutResponseDTO { */ private String definition; - private MetadataResponseDTO metadata; + private MetadataResponseDto metadata; } diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/MetadataResponseDTO.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/MetadataResponseDto.java similarity index 73% rename from layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/MetadataResponseDTO.java rename to layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/MetadataResponseDto.java index 21dc8082d..0e20e9066 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/MetadataResponseDTO.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/MetadataResponseDto.java @@ -1,12 +1,13 @@ package org.finos.vuu.layoutserver.dto.response; +import lombok.Data; +import org.finos.vuu.layoutserver.dto.MetadataDto; + import java.util.Date; import java.util.UUID; -import lombok.Data; -import org.finos.vuu.layoutserver.dto.MetadataDTO; @Data -public class MetadataResponseDTO implements MetadataDTO { +public class MetadataResponseDto implements MetadataDto { private UUID layoutId; private String name; diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java index 03a9cf640..4ba654e25 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java @@ -1,18 +1,9 @@ package org.finos.vuu.layoutserver.controller; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.ArrayList; -import java.util.List; -import java.util.NoSuchElementException; -import java.util.UUID; -import org.finos.vuu.layoutserver.dto.request.LayoutRequestDTO; -import org.finos.vuu.layoutserver.dto.request.MetadataRequestDTO; -import org.finos.vuu.layoutserver.dto.response.LayoutResponseDTO; -import org.finos.vuu.layoutserver.dto.response.MetadataResponseDTO; +import org.finos.vuu.layoutserver.dto.request.LayoutRequestDto; +import org.finos.vuu.layoutserver.dto.request.MetadataRequestDto; +import org.finos.vuu.layoutserver.dto.response.LayoutResponseDto; +import org.finos.vuu.layoutserver.dto.response.MetadataResponseDto; import org.finos.vuu.layoutserver.model.Layout; import org.finos.vuu.layoutserver.model.Metadata; import org.finos.vuu.layoutserver.service.LayoutService; @@ -24,6 +15,16 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.modelmapper.ModelMapper; +import java.util.ArrayList; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + @ExtendWith(MockitoExtension.class) class LayoutControllerTest { @@ -40,9 +41,9 @@ class LayoutControllerTest { private UUID doesNotExistLayoutId; private Layout layout; private Metadata metadata; - private LayoutRequestDTO layoutRequest; - private LayoutResponseDTO expectedLayoutResponse; - private List expectedMetadataResponse; + private LayoutRequestDto layoutRequest; + private LayoutResponseDto expectedLayoutResponse; + private List expectedMetadataResponse; @BeforeEach public void setup() { @@ -63,8 +64,8 @@ public void setup() { layout.setDefinition(layoutDefinition); layout.setMetadata(metadata); - layoutRequest = new LayoutRequestDTO(); - MetadataRequestDTO metadataRequestDTO = new MetadataRequestDTO(); + layoutRequest = new LayoutRequestDto(); + MetadataRequestDto metadataRequestDTO = new MetadataRequestDto(); metadataRequestDTO.setName(metadata.getName()); metadataRequestDTO.setUser(metadata.getUser()); metadataRequestDTO.setGroup(metadata.getGroup()); @@ -72,11 +73,11 @@ public void setup() { layoutRequest.setDefinition(layout.getDefinition()); layoutRequest.setMetadata(metadataRequestDTO); - expectedLayoutResponse = new LayoutResponseDTO(); + expectedLayoutResponse = new LayoutResponseDto(); expectedLayoutResponse.setId(layout.getId()); expectedLayoutResponse.setDefinition(layout.getDefinition()); - MetadataResponseDTO metadataResponse = getMetadataResponseDTO(); + MetadataResponseDto metadataResponse = getMetadataResponseDTO(); expectedLayoutResponse.setMetadata(metadataResponse); expectedMetadataResponse = new ArrayList<>(); @@ -87,7 +88,7 @@ public void setup() { @Test void getLayout_layoutExists_returnsLayout() { when(layoutService.getLayout(validLayoutId)).thenReturn(layout); - when(modelMapper.map(layout, LayoutResponseDTO.class)).thenReturn( + when(modelMapper.map(layout, LayoutResponseDto.class)).thenReturn( expectedLayoutResponse); assertThat(layoutController.getLayout(validLayoutId)).isEqualTo(expectedLayoutResponse); } @@ -102,7 +103,7 @@ void getLayout_layoutDoesNotExist_throwsNotFoundAndReturns404() { @Test void getMetadata_metadataExists_returnsMetadata() { when(layoutService.getMetadata()).thenReturn(List.of(metadata)); - when(modelMapper.map(metadata, MetadataResponseDTO.class)).thenReturn( + when(modelMapper.map(metadata, MetadataResponseDto.class)).thenReturn( getMetadataResponseDTO()); assertThat(layoutController.getMetadata()).isEqualTo(expectedMetadataResponse); } @@ -122,7 +123,7 @@ void createLayout_validLayout_returnsCreatedLayout() { when(modelMapper.map(layoutRequest, Layout.class)).thenReturn(layoutWithoutIds); when(layoutService.createLayout(layoutWithoutIds)).thenReturn(layout.getId()); when(layoutService.getLayout(layout.getId())).thenReturn(layout); - when(modelMapper.map(layout, LayoutResponseDTO.class)).thenReturn(expectedLayoutResponse); + when(modelMapper.map(layout, LayoutResponseDto.class)).thenReturn(expectedLayoutResponse); assertThat(layoutController.createLayout(layoutRequest)) .isEqualTo(expectedLayoutResponse); @@ -148,8 +149,8 @@ void deleteLayout_callsLayoutService() { verify(layoutService).deleteLayout(validLayoutId); } - private MetadataResponseDTO getMetadataResponseDTO() { - MetadataResponseDTO metadataResponse = new MetadataResponseDTO(); + private MetadataResponseDto getMetadataResponseDTO() { + MetadataResponseDto metadataResponse = new MetadataResponseDto(); metadataResponse.setLayoutId(layout.getId()); metadataResponse.setName(layout.getMetadata().getName()); metadataResponse.setUser(layout.getMetadata().getUser()); diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/LayoutIntegrationTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/LayoutIntegrationTest.java index d0297ad5f..15ea622a6 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/LayoutIntegrationTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/LayoutIntegrationTest.java @@ -1,19 +1,9 @@ package org.finos.vuu.layoutserver.integration; -import static org.assertj.core.api.Assertions.assertThat; -import static org.hamcrest.Matchers.is; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - import com.fasterxml.jackson.databind.ObjectMapper; import com.jayway.jsonpath.JsonPath; -import java.util.UUID; -import org.finos.vuu.layoutserver.dto.request.LayoutRequestDTO; -import org.finos.vuu.layoutserver.dto.request.MetadataRequestDTO; +import org.finos.vuu.layoutserver.dto.request.LayoutRequestDto; +import org.finos.vuu.layoutserver.dto.request.MetadataRequestDto; import org.finos.vuu.layoutserver.model.Layout; import org.finos.vuu.layoutserver.model.Metadata; import org.finos.vuu.layoutserver.repository.LayoutRepository; @@ -29,6 +19,17 @@ import org.springframework.test.web.servlet.MvcResult; import org.springframework.transaction.annotation.Transactional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.is; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + @SpringBootTest @AutoConfigureMockMvc @Transactional @@ -106,7 +107,7 @@ void getMetadata_metadataDoesNotExist_returnsEmptyList() throws Exception { @Test void createLayout_validLayout_returnsCreatedLayoutAndLayoutIsPersisted() throws Exception { - LayoutRequestDTO layoutRequest = createValidCreateLayoutRequest(); + LayoutRequestDto layoutRequest = createValidCreateLayoutRequest(); MvcResult result = mockMvc.perform(post("/layouts") .content(objectMapper.writeValueAsString(layoutRequest)) @@ -154,7 +155,7 @@ void createLayout_invalidLayout_returns400() throws Exception { @Test void createLayout_validLayoutButInvalidMetadata_returns400AndDoesNotCreateLayout() throws Exception { - LayoutRequestDTO layoutRequest = createValidCreateLayoutRequest(); + LayoutRequestDto layoutRequest = createValidCreateLayoutRequest(); layoutRequest.setMetadata(null); mockMvc.perform(post("/layouts") @@ -168,7 +169,7 @@ void createLayout_validLayoutButInvalidMetadata_returns400AndDoesNotCreateLayout @Test void updateLayout_validIDAndValidRequest_returns204AndLayoutHasChanged() throws Exception { Layout layout = createDefaultLayoutInDatabase(); - LayoutRequestDTO layoutRequest = createValidUpdateRequest(); + LayoutRequestDto layoutRequest = createValidUpdateRequest(); mockMvc.perform(put("/layouts/{id}", layout.getId()) .content(objectMapper.writeValueAsString(layoutRequest)) @@ -195,7 +196,7 @@ void updateLayout_invalidRequestBodyDefinitionIsBlankAndMetadataIsNull_returns40 throws Exception { Layout layout = createDefaultLayoutInDatabase(); - LayoutRequestDTO request = new LayoutRequestDTO(); + LayoutRequestDto request = new LayoutRequestDto(); request.setDefinition(""); request.setMetadata(null); @@ -224,7 +225,7 @@ void updateLayout_invalidRequestBodyUnexpectedFormat_returns400AndLayoutDoesNotC @Test void updateLayout_validIdButLayoutDoesNotExist_returnsNotFound() throws Exception { UUID layoutID = UUID.randomUUID(); - LayoutRequestDTO layoutRequest = createValidUpdateRequest(); + LayoutRequestDto layoutRequest = createValidUpdateRequest(); mockMvc.perform(put("/layouts/{id}", layoutID) .content(objectMapper.writeValueAsString(layoutRequest)) @@ -235,7 +236,7 @@ void updateLayout_validIdButLayoutDoesNotExist_returnsNotFound() throws Exceptio @Test void updateLayout_invalidId_returns400() throws Exception { String layoutID = "invalidUUID"; - LayoutRequestDTO layoutRequest = createValidUpdateRequest(); + LayoutRequestDto layoutRequest = createValidUpdateRequest(); mockMvc.perform(put("/layouts/{id}", layoutID) .content(objectMapper.writeValueAsString(layoutRequest)) @@ -289,27 +290,27 @@ private Layout createDefaultLayoutInDatabase() { return createdLayout; } - private LayoutRequestDTO createValidUpdateRequest() { - MetadataRequestDTO metadataRequest = new MetadataRequestDTO(); + private LayoutRequestDto createValidUpdateRequest() { + MetadataRequestDto metadataRequest = new MetadataRequestDto(); metadataRequest.setName("Updated name"); metadataRequest.setGroup("Updated group"); metadataRequest.setScreenshot("Updated screenshot"); metadataRequest.setUser("Updated user"); - LayoutRequestDTO layoutRequest = new LayoutRequestDTO(); + LayoutRequestDto layoutRequest = new LayoutRequestDto(); layoutRequest.setDefinition("Updated definition"); layoutRequest.setMetadata(metadataRequest); return layoutRequest; } - private LayoutRequestDTO createValidCreateLayoutRequest() { - MetadataRequestDTO metadataRequest = new MetadataRequestDTO(); + private LayoutRequestDto createValidCreateLayoutRequest() { + MetadataRequestDto metadataRequest = new MetadataRequestDto(); metadataRequest.setName(defaultName); metadataRequest.setGroup(defaultGroup); metadataRequest.setScreenshot(defaultScreenshot); metadataRequest.setUser(defaultUser); - LayoutRequestDTO layoutRequest = new LayoutRequestDTO(); + LayoutRequestDto layoutRequest = new LayoutRequestDto(); layoutRequest.setDefinition(defaultDefinition); layoutRequest.setMetadata(metadataRequest); return layoutRequest; From 5fbc41b92df9743f8f7168b58ed49aa5e0e1af2e Mon Sep 17 00:00:00 2001 From: Peter Ling Date: Fri, 20 Oct 2023 17:14:35 +0100 Subject: [PATCH 21/56] VUU-70: Return 500 on failed JSON read --- .../controller/GlobalExceptionHandler.java | 14 ++++++++++---- .../service/ApplicationLayoutService.java | 3 +-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/GlobalExceptionHandler.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/GlobalExceptionHandler.java index a41a76ffa..e3c9a0345 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/GlobalExceptionHandler.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/GlobalExceptionHandler.java @@ -1,5 +1,6 @@ package org.finos.vuu.layoutserver.controller; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; @@ -12,12 +13,17 @@ public class GlobalExceptionHandler { @ExceptionHandler(NoSuchElementException.class) - public ResponseEntity handleNotFound(NoSuchElementException ex) { - return new ResponseEntity<>(ex.getMessage(), org.springframework.http.HttpStatus.NOT_FOUND); + public ResponseEntity handleNotFound(Exception ex) { + return new ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND); } @ExceptionHandler({MethodArgumentNotValidException.class, MethodArgumentTypeMismatchException.class}) - public ResponseEntity handleBadRequest(MethodArgumentNotValidException ex) { - return new ResponseEntity<>(ex.getMessage(), org.springframework.http.HttpStatus.BAD_REQUEST); + public ResponseEntity handleBadRequest(Exception ex) { + return new ResponseEntity<>(ex.getMessage(), HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(RuntimeException.class) + public ResponseEntity handleInternalServerError(Exception ex) { + return new ResponseEntity<>(ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); } } diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java index 8f019f0cb..915717803 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java @@ -64,8 +64,7 @@ private JsonNode loadDefaultLayoutJsonFile() { ClassPathResource resource = new ClassPathResource(DEFAULT_LAYOUT_FILE); return objectMapper.readTree(resource.getInputStream()); } catch (IOException e) { - logger.warn("Failed to read default application layout, returning empty node"); - return objectMapper.createObjectNode(); + throw new RuntimeException("Failed to read default application layout"); } } } From 27ceb94cecbec612875802b2249237e9fc77999f Mon Sep 17 00:00:00 2001 From: Peter Ling Date: Mon, 23 Oct 2023 11:41:17 +0100 Subject: [PATCH 22/56] VUU-70: Adjust exception handling --- .../vuu/layoutserver/controller/GlobalExceptionHandler.java | 5 ----- layout-server/src/main/resources/application.properties | 1 + layout-server/src/test/resources/application-test.properties | 1 + 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/GlobalExceptionHandler.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/GlobalExceptionHandler.java index e3c9a0345..830163c2f 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/GlobalExceptionHandler.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/GlobalExceptionHandler.java @@ -21,9 +21,4 @@ public ResponseEntity handleNotFound(Exception ex) { public ResponseEntity handleBadRequest(Exception ex) { return new ResponseEntity<>(ex.getMessage(), HttpStatus.BAD_REQUEST); } - - @ExceptionHandler(RuntimeException.class) - public ResponseEntity handleInternalServerError(Exception ex) { - return new ResponseEntity<>(ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); - } } diff --git a/layout-server/src/main/resources/application.properties b/layout-server/src/main/resources/application.properties index afee88372..621ba95d8 100644 --- a/layout-server/src/main/resources/application.properties +++ b/layout-server/src/main/resources/application.properties @@ -1,3 +1,4 @@ +server.error.include-message=always server.port=8081 server.servlet.contextPath=/api springdoc.swagger-ui.path=/swagger diff --git a/layout-server/src/test/resources/application-test.properties b/layout-server/src/test/resources/application-test.properties index 2722b4aca..13ca6c535 100644 --- a/layout-server/src/test/resources/application-test.properties +++ b/layout-server/src/test/resources/application-test.properties @@ -1,3 +1,4 @@ +server.error.include-message=always spring.datasource.url=jdbc:h2:mem:testdb;NON_KEYWORDS=GROUP,USER spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa From 4d99722b29961f3b2a6fe4180d13e1a2b50cf30e Mon Sep 17 00:00:00 2001 From: vferraro-scottlogic Date: Mon, 23 Oct 2023 13:10:04 +0100 Subject: [PATCH 23/56] VUU-27 add check for date --- .../layout-persistence/LocalLayoutPersistenceManager.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vuu-ui/packages/vuu-layout/test/layout-persistence/LocalLayoutPersistenceManager.test.ts b/vuu-ui/packages/vuu-layout/test/layout-persistence/LocalLayoutPersistenceManager.test.ts index bb0a5bdea..deb20c3d3 100644 --- a/vuu-ui/packages/vuu-layout/test/layout-persistence/LocalLayoutPersistenceManager.test.ts +++ b/vuu-ui/packages/vuu-layout/test/layout-persistence/LocalLayoutPersistenceManager.test.ts @@ -70,7 +70,7 @@ afterEach(() => { }) describe("createLayout", () => { - it("persists to local storage with a unique ID", async () => { + it("persists to local storage with a unique ID and current date", async () => { const { id, created } = await persistenceManager.createLayout( metadataToAdd, layoutToAdd @@ -91,6 +91,7 @@ describe("createLayout", () => { id, }; + expect(created).toEqual(formatDate(new Date(), "dd.mm.yyyy")); expect(persistedMetadata).toEqual([expectedMetadata]); expect(persistedLayout).toEqual([expectedLayout]); }); From a3b63ffdb586ca38de9d07eae38c9cf175d28451 Mon Sep 17 00:00:00 2001 From: vferraro-scottlogic Date: Mon, 23 Oct 2023 15:00:05 +0100 Subject: [PATCH 24/56] VUU-27 allow BASE_URL env variable --- .../src/layout-persistence/RemoteLayoutPersistenceManager.ts | 4 +++- .../vuu-shell/src/layout-management/useLayoutManager.tsx | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts b/vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts index 06f6fa4ba..3e35b1f3b 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts @@ -3,7 +3,9 @@ import { LayoutPersistenceManager } from "./LayoutPersistenceManager"; import { LayoutJSON } from "../layout-reducer"; import { defaultLayout } from "./data"; -const baseURL = "http://127.0.0.1:8081/api"; +const DEFAULT_SERVER_BASE_URL = "http://127.0.0.1:8081/api" + +const baseURL = process.env.LAYOUT_BASE_URL ?? DEFAULT_SERVER_BASE_URL; const metadataSaveLocation = "layouts/metadata"; const layoutsSaveLocation = "layouts"; diff --git a/vuu-ui/packages/vuu-shell/src/layout-management/useLayoutManager.tsx b/vuu-ui/packages/vuu-shell/src/layout-management/useLayoutManager.tsx index 28c8a6df7..1d6ad8e62 100644 --- a/vuu-ui/packages/vuu-shell/src/layout-management/useLayoutManager.tsx +++ b/vuu-ui/packages/vuu-shell/src/layout-management/useLayoutManager.tsx @@ -3,7 +3,7 @@ import { LayoutJSON, LocalLayoutPersistenceManager, resolveJSONPath, RemoteLayou import { LayoutMetadata, LayoutMetadataDto } from "./layoutTypes"; import { defaultLayout } from "@finos/vuu-layout/"; -const local = process.env.LOCAL || false; +const local = process.env.LOCAL ?? true; const persistenceManager = local ? new LocalLayoutPersistenceManager() : new RemoteLayoutPersistenceManager(); From 8602b7035ee0a347cfac05f6956df3317553ddb0 Mon Sep 17 00:00:00 2001 From: Peter Ling Date: Mon, 23 Oct 2023 15:57:58 +0100 Subject: [PATCH 25/56] VUU-70: Implement custom error responses --- .../controller/GlobalExceptionHandler.java | 22 ++++++++++++++---- .../dto/response/ErrorResponse.java | 23 +++++++++++++++++++ .../InternalServerErrorException.java | 7 ++++++ .../service/ApplicationLayoutService.java | 5 ++-- .../ApplicationLayoutIntegrationTest.java | 6 ++--- 5 files changed, 53 insertions(+), 10 deletions(-) create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/ErrorResponse.java create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/exceptions/InternalServerErrorException.java diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/GlobalExceptionHandler.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/GlobalExceptionHandler.java index 830163c2f..e3aa1bcd7 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/GlobalExceptionHandler.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/GlobalExceptionHandler.java @@ -1,5 +1,7 @@ package org.finos.vuu.layoutserver.controller; +import org.finos.vuu.layoutserver.dto.response.ErrorResponse; +import org.finos.vuu.layoutserver.exceptions.InternalServerErrorException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; @@ -7,18 +9,30 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import javax.servlet.http.HttpServletRequest; import java.util.NoSuchElementException; @ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(NoSuchElementException.class) - public ResponseEntity handleNotFound(Exception ex) { - return new ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND); + public ResponseEntity handleNotFound(HttpServletRequest request, Exception ex) { + return generateResponse(request, ex, HttpStatus.NOT_FOUND); } @ExceptionHandler({MethodArgumentNotValidException.class, MethodArgumentTypeMismatchException.class}) - public ResponseEntity handleBadRequest(Exception ex) { - return new ResponseEntity<>(ex.getMessage(), HttpStatus.BAD_REQUEST); + public ResponseEntity handleBadRequest(Exception ex, HttpServletRequest request) { + return generateResponse(request, ex, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(InternalServerErrorException.class) + public ResponseEntity handleInternalServerError(HttpServletRequest request, Exception ex) { + return generateResponse(request, ex, HttpStatus.INTERNAL_SERVER_ERROR); + } + + private ResponseEntity generateResponse(HttpServletRequest request, + Exception ex, + HttpStatus status) { + return new ResponseEntity<>(new ErrorResponse(request, ex, status), status); } } diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/ErrorResponse.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/ErrorResponse.java new file mode 100644 index 000000000..fb2576489 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/ErrorResponse.java @@ -0,0 +1,23 @@ +package org.finos.vuu.layoutserver.dto.response; + +import lombok.Data; +import org.springframework.http.HttpStatus; + +import javax.servlet.http.HttpServletRequest; +import java.util.Date; + +@Data +public class ErrorResponse { + private Date timestamp = new Date(); + private int status; + private String error; + private String message; + private String path; + + public ErrorResponse(HttpServletRequest request, Exception ex, HttpStatus status) { + this.status = status.value(); + this.error = status.getReasonPhrase(); + this.path = request.getRequestURI(); + this.message = ex.getMessage(); + } +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/exceptions/InternalServerErrorException.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/exceptions/InternalServerErrorException.java new file mode 100644 index 000000000..b1164eab6 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/exceptions/InternalServerErrorException.java @@ -0,0 +1,7 @@ +package org.finos.vuu.layoutserver.exceptions; + +public class InternalServerErrorException extends RuntimeException { + public InternalServerErrorException(String message) { + super(message); + } +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java index 915717803..52ca46c03 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; +import org.finos.vuu.layoutserver.exceptions.InternalServerErrorException; import org.finos.vuu.layoutserver.model.ApplicationLayout; import org.finos.vuu.layoutserver.repository.ApplicationLayoutRepository; import org.slf4j.Logger; @@ -60,11 +61,11 @@ private void loadDefaultLayout() { private JsonNode loadDefaultLayoutJsonFile() { ObjectMapper objectMapper = new ObjectMapper(); + ClassPathResource resource = new ClassPathResource(DEFAULT_LAYOUT_FILE); try { - ClassPathResource resource = new ClassPathResource(DEFAULT_LAYOUT_FILE); return objectMapper.readTree(resource.getInputStream()); } catch (IOException e) { - throw new RuntimeException("Failed to read default application layout"); + throw new InternalServerErrorException("Failed to read default application layout"); } } } diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java index a0c5837f2..11eeaffb5 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java @@ -151,12 +151,10 @@ public void updateApplicationLayout_layoutExists_returns204AndOverwritesLayout() public void deleteApplicationLayout_noLayoutExists_returns404() throws Exception { String user = "user"; - String response = mockMvc.perform(delete("/application-layouts") + mockMvc.perform(delete("/application-layouts") .header("user", user)) .andExpect(status().isNotFound()) - .andReturn().getResponse().getContentAsString(); - - assertThat(response).isEqualTo("No layout found for user: " + user); + .andExpect(jsonPath("$.message", is("No layout found for user: " + user))); } @Test From 4a8b62e4ee3305735d03edf5880702707c1667ee Mon Sep 17 00:00:00 2001 From: Peter Ling Date: Mon, 23 Oct 2023 17:12:14 +0100 Subject: [PATCH 26/56] VUU-70: Update javadocs --- .../controller/ApplicationLayoutController.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java index dae53281d..b0b4d67f9 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java @@ -27,7 +27,9 @@ public class ApplicationLayoutController { private final ModelMapper mapper; /** - * Gets the application layout for the requesting user. Returns a default layout if none exists. + * Gets the persisted application layout for the requesting user. If the requesting user does not have an + * application layout persisted, a default layout with a null username is returned instead. No more than one + * application layout can be persisted for a given user. * * @return the application layout */ @@ -38,7 +40,8 @@ public ApplicationLayoutDto getApplicationLayout(@RequestHeader("user") String u } /** - * Creates a new application layout for the requesting user. + * Creates a new application layout for the requesting user. If the requesting user already has an application + * layout persisted, this will be identical to the PUT method, {@link #updateApplicationLayout(String, JsonNode)}. * * @param layoutDefinition JSON representation of the application layout to be created * @param username the user making the request @@ -50,7 +53,8 @@ public void createApplicationLayout(@RequestHeader("user") String username, @Req } /** - * Updates the application layout for the requesting user. + * Updates the application layout for the requesting user. If the requesting user does not have an application + * layout persisted, this will be identical to the POST method, {@link #createApplicationLayout(String, JsonNode)}. * * @param layoutDefinition JSON representation of the application layout to be created * @param username the user making the request @@ -62,7 +66,8 @@ public void updateApplicationLayout(@RequestHeader("user") String username, @Req } /** - * Deletes the application layout for the requesting user. + * Deletes the application layout for the requesting user. A 404 will be returned if there is no existing + * application layout. * * @param username the user making the request */ From 3772c26c031330de51b2078c8f59a7360aed6a02 Mon Sep 17 00:00:00 2001 From: Peter Ling Date: Tue, 24 Oct 2023 08:49:43 +0100 Subject: [PATCH 27/56] VUU-70: Revert application properties --- layout-server/src/main/resources/application.properties | 1 - layout-server/src/test/resources/application-test.properties | 1 - 2 files changed, 2 deletions(-) diff --git a/layout-server/src/main/resources/application.properties b/layout-server/src/main/resources/application.properties index 621ba95d8..afee88372 100644 --- a/layout-server/src/main/resources/application.properties +++ b/layout-server/src/main/resources/application.properties @@ -1,4 +1,3 @@ -server.error.include-message=always server.port=8081 server.servlet.contextPath=/api springdoc.swagger-ui.path=/swagger diff --git a/layout-server/src/test/resources/application-test.properties b/layout-server/src/test/resources/application-test.properties index 13ca6c535..2722b4aca 100644 --- a/layout-server/src/test/resources/application-test.properties +++ b/layout-server/src/test/resources/application-test.properties @@ -1,4 +1,3 @@ -server.error.include-message=always spring.datasource.url=jdbc:h2:mem:testdb;NON_KEYWORDS=GROUP,USER spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa From 2c548ada8a110ddb2015ca67b669596b749c2df0 Mon Sep 17 00:00:00 2001 From: vferraro-scottlogic Date: Tue, 24 Oct 2023 09:22:03 +0100 Subject: [PATCH 28/56] VUU-27 test improvements --- .../layout-persistence/LocalLayoutPersistenceManager.test.ts | 2 +- .../layout-persistence/RemoteLayoutPersistenceManager.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/vuu-ui/packages/vuu-layout/test/layout-persistence/LocalLayoutPersistenceManager.test.ts b/vuu-ui/packages/vuu-layout/test/layout-persistence/LocalLayoutPersistenceManager.test.ts index deb20c3d3..1e7a156b5 100644 --- a/vuu-ui/packages/vuu-layout/test/layout-persistence/LocalLayoutPersistenceManager.test.ts +++ b/vuu-ui/packages/vuu-layout/test/layout-persistence/LocalLayoutPersistenceManager.test.ts @@ -91,7 +91,7 @@ describe("createLayout", () => { id, }; - expect(created).toEqual(formatDate(new Date(), "dd.mm.yyyy")); + expect(created).toEqual(newDate); expect(persistedMetadata).toEqual([expectedMetadata]); expect(persistedLayout).toEqual([expectedLayout]); }); diff --git a/vuu-ui/packages/vuu-layout/test/layout-persistence/RemoteLayoutPersistenceManager.test.ts b/vuu-ui/packages/vuu-layout/test/layout-persistence/RemoteLayoutPersistenceManager.test.ts index 6c5c4868e..3910da0ee 100644 --- a/vuu-ui/packages/vuu-layout/test/layout-persistence/RemoteLayoutPersistenceManager.test.ts +++ b/vuu-ui/packages/vuu-layout/test/layout-persistence/RemoteLayoutPersistenceManager.test.ts @@ -15,7 +15,7 @@ const metadata: LayoutMetadata[] = [ id: "0001", name: "layout 1", group: "group 1", - screenshot: "", + screenshot: "screenshot", user: "username", created: "01.01.2000", }, From 9b08ad181c46eb73a129d5da05f053d76e9378d0 Mon Sep 17 00:00:00 2001 From: keikeicheung Date: Tue, 17 Oct 2023 14:52:23 +0800 Subject: [PATCH 29/56] #900 disable failing test --- .../org/finos/vuu/provider/BasketConstituentProviderTest.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vuu/src/test/scala/org/finos/vuu/provider/BasketConstituentProviderTest.scala b/vuu/src/test/scala/org/finos/vuu/provider/BasketConstituentProviderTest.scala index 30465783f..e20218983 100644 --- a/vuu/src/test/scala/org/finos/vuu/provider/BasketConstituentProviderTest.scala +++ b/vuu/src/test/scala/org/finos/vuu/provider/BasketConstituentProviderTest.scala @@ -65,7 +65,7 @@ class BasketConstituentProviderTest extends AnyFeatureSpec with Matchers with Be } } - Feature("Able to load basket constituents from .NASDAQ100 and show on basket constituent table") { + ignore("Able to load basket constituents from .NASDAQ100 and show on basket constituent table") { // Scenario("display ric") { // val array = getDataForBasket(".NASDAQ100") From 671bb06cf65246fb40282c56f869a185fd49da5f Mon Sep 17 00:00:00 2001 From: heswell Date: Wed, 18 Oct 2023 08:51:58 +0100 Subject: [PATCH 30/56] Update check on viewport and context menu (#913) * remove reprecated Portal, fixes in COntextMenu * use woff2 font, fix portal * make sure ContextMenu always has theme attributes * type fixes --- .../vuu-data/src/connection-manager.ts | 1 + .../array-backed-moving-window.ts | 23 ++++- .../vuu-data/src/server-proxy/viewport.ts | 3 +- vuu-ui/packages/vuu-data/src/worker.ts | 5 +- .../vuu-data/test/server-proxy.test.ts | 26 +++--- vuu-ui/packages/vuu-data/test/test-utils.ts | 5 +- .../src/context-menu/useContextMenu.ts | 2 +- .../filter-builder-menu/FilterBuilderMenu.tsx | 2 +- .../filter-clause/useFilterClauseEditor.ts | 23 +++-- .../vuu-layout/src/drag-drop/Draggable.ts | 2 +- .../packages/vuu-popups/src/dialog/Dialog.tsx | 21 +---- .../vuu-popups/src/menu/ContextMenu.css | 7 -- .../vuu-popups/src/menu/ContextMenu.tsx | 77 +++++++++------- .../packages/vuu-popups/src/menu/MenuList.css | 2 +- .../packages/vuu-popups/src/menu/MenuList.tsx | 19 ++-- .../vuu-popups/src/menu/list-dom-utils.ts | 7 +- .../vuu-popups/src/menu/use-cascade.ts | 75 ++++++++++++---- .../src/menu/use-keyboard-navigation.ts | 7 +- .../vuu-popups/src/menu/useContextMenu.tsx | 39 ++++----- vuu-ui/packages/vuu-popups/src/menu/utils.ts | 2 +- .../vuu-popups/src/popup-menu/PopupMenu.tsx | 20 ++++- .../packages/vuu-popups/src/popup/Popup.tsx | 16 ++-- .../vuu-popups/src/popup/popup-service.ts | 82 +----------------- .../src/popup/useAnchoredPosition.ts | 35 ++++---- .../portal-deprecated/PortalDeprecated.tsx | 58 ------------- .../vuu-popups/src/portal-deprecated/index.ts | 2 - .../src/portal-deprecated/portal-utils.ts | 9 -- .../packages/vuu-popups/src/portal/Portal.css | 10 --- .../packages/vuu-popups/src/portal/Portal.tsx | 23 ++++- .../src/theme-provider/ThemeProvider.tsx | 22 ++++- .../src/table-next/column-menu/ColumnMenu.css | 8 -- .../src/table-next/column-menu/ColumnMenu.tsx | 1 - .../vuu-table/src/table/dataTableTypes.ts | 2 +- .../vuu-table/src/table/useSelection.ts | 2 +- .../packages/vuu-table/src/table/useTable.ts | 4 - vuu-ui/packages/vuu-theme/css/global.css | 12 --- .../packages/vuu-theme/fonts/NunitoSans.css | 58 +++++++++++++ .../vuu-theme/fonts/NunitoSansv15.woff2 | Bin 0 -> 22144 bytes vuu-ui/packages/vuu-theme/index.css | 1 + .../src/drag-drop/Draggable.tsx | 33 +++++-- .../src/drag-drop/DropIndicator.tsx | 6 +- .../common-hooks/useKeyboardNavigation.ts | 2 + .../vuu-ui-controls/src/list/useList.ts | 10 +-- .../app-vuu-basket-trader/login.css | 1 - .../app-vuu-basket-trader/public/demo.html | 3 - .../app-vuu-basket-trader/public/index.html | 3 - .../app-vuu-basket-trader/public/login.html | 3 - vuu-ui/sample-apps/app-vuu-example/login.css | 1 - .../Filters/FilterBar/FilterBar.examples.tsx | 1 + vuu-ui/showcase/src/examples/Layout/index.ts | 1 - .../examples/Popups/ContextMenu.examples.tsx | 7 +- .../{Layout => Popups}/Dialog.examples.tsx | 0 vuu-ui/showcase/src/examples/Popups/index.ts | 1 + .../examples/UiControls/Dropdown.examples.tsx | 12 +-- .../src/examples/UiControls/List.examples.tsx | 4 +- vuu-ui/showcase/src/index.css | 11 ++- vuu-ui/showcase/src/index.tsx | 2 +- vuu-ui/showcase/templates/index-preview.html | 3 - vuu-ui/showcase/templates/index.html | 3 - 59 files changed, 409 insertions(+), 411 deletions(-) delete mode 100644 vuu-ui/packages/vuu-popups/src/menu/ContextMenu.css delete mode 100644 vuu-ui/packages/vuu-popups/src/portal-deprecated/PortalDeprecated.tsx delete mode 100644 vuu-ui/packages/vuu-popups/src/portal-deprecated/portal-utils.ts create mode 100644 vuu-ui/packages/vuu-theme/fonts/NunitoSans.css create mode 100644 vuu-ui/packages/vuu-theme/fonts/NunitoSansv15.woff2 rename vuu-ui/showcase/src/examples/{Layout => Popups}/Dialog.examples.tsx (100%) diff --git a/vuu-ui/packages/vuu-data/src/connection-manager.ts b/vuu-ui/packages/vuu-data/src/connection-manager.ts index ce9d3b15b..086cc3763 100644 --- a/vuu-ui/packages/vuu-data/src/connection-manager.ts +++ b/vuu-ui/packages/vuu-data/src/connection-manager.ts @@ -164,6 +164,7 @@ function handleMessageFromWorker({ } else if (isConnectionStatusMessage(message)) { ConnectionManager.emit("connection-status", message); } else if (isConnectionQualityMetrics(message)) { + console.log({ message }); ConnectionManager.emit("connection-metrics", message); } else { const requestId = (message as VuuUIMessageInRPC).requestId; diff --git a/vuu-ui/packages/vuu-data/src/server-proxy/array-backed-moving-window.ts b/vuu-ui/packages/vuu-data/src/server-proxy/array-backed-moving-window.ts index a5357ecd5..682a332a1 100644 --- a/vuu-ui/packages/vuu-data/src/server-proxy/array-backed-moving-window.ts +++ b/vuu-ui/packages/vuu-data/src/server-proxy/array-backed-moving-window.ts @@ -7,6 +7,23 @@ type RangeTuple = [boolean, readonly VuuRow[] /*, readonly VuuRow[]*/]; const log = logger("array-backed-moving-window"); +function dataIsUnchanged(newRow: VuuRow, existingRow?: VuuRow) { + if (!existingRow) { + return false; + } + + if (existingRow.sel !== newRow.sel) { + return false; + } + + for (let i = 0; i < existingRow.data.length; i++) { + if (existingRow.data[i] !== newRow.data[i]) { + return false; + } + } + return true; +} + export class ArrayBackedMovingWindow { #range: WindowRange; @@ -79,9 +96,13 @@ export class ArrayBackedMovingWindow { setAtIndex(row: VuuRow) { const { rowIndex: index } = row; + const internalIndex = index - this.#range.from; + //TODO measure the performance impact of this check + if (dataIsUnchanged(row, this.internalData[internalIndex])) { + return false; + } const isWithinClientRange = this.isWithinClientRange(index); if (isWithinClientRange || this.isWithinRange(index)) { - const internalIndex = index - this.#range.from; if (!this.internalData[internalIndex] && isWithinClientRange) { this.rowsWithinRange += 1; } diff --git a/vuu-ui/packages/vuu-data/src/server-proxy/viewport.ts b/vuu-ui/packages/vuu-data/src/server-proxy/viewport.ts index 4a51c0a71..80b7c2a37 100644 --- a/vuu-ui/packages/vuu-data/src/server-proxy/viewport.ts +++ b/vuu-ui/packages/vuu-data/src/server-proxy/viewport.ts @@ -258,7 +258,6 @@ export class Viewport { if (lastMode === mode) { const ts = Date.now(); - console.log(`read data now ${ts}`); this.lastUpdateStatus.count += 1; this.lastUpdateStatus.ts = ts; elapsedTime = lastTS === 0 ? 0 : ts - lastTS; @@ -930,7 +929,7 @@ export class Viewport { private throttleMessage = (mode: DataUpdateMode) => { if (this.shouldThrottleMessage(mode)) { - console.log("throttling updates setTimeout to 2000"); + info?.("throttling updates setTimeout to 2000"); if (this.updateThrottleTimer === undefined) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore diff --git a/vuu-ui/packages/vuu-data/src/worker.ts b/vuu-ui/packages/vuu-data/src/worker.ts index 35952e442..881746c68 100644 --- a/vuu-ui/packages/vuu-data/src/worker.ts +++ b/vuu-ui/packages/vuu-data/src/worker.ts @@ -36,9 +36,10 @@ async function connectToServer( // never be called until subscriptions have been made, so this is safe. //TODO do we need to listen in to the connection messages here so we can lock back in, in the event of a reconnenct ? (msg) => { - if (isConnectionQualityMetrics(msg)) + if (isConnectionQualityMetrics(msg)) { + console.log("post connection metrics"); postMessage({ type: "connection-metrics", messages: msg }); - else if (isConnectionStatusMessage(msg)) { + } else if (isConnectionStatusMessage(msg)) { onConnectionStatusChange(msg); if (msg.status === "reconnected") { server.reconnect(); diff --git a/vuu-ui/packages/vuu-data/test/server-proxy.test.ts b/vuu-ui/packages/vuu-data/test/server-proxy.test.ts index fc78984e7..0824c8771 100644 --- a/vuu-ui/packages/vuu-data/test/server-proxy.test.ts +++ b/vuu-ui/packages/vuu-data/test/server-proxy.test.ts @@ -2574,9 +2574,9 @@ describe("ServerProxy", () => { body: { ...COMMON_TABLE_ROW_ATTRS, rows: [ - ...createTableRows("server-vp-1", 0, 1, 100, 1, 1), + ...createTableRows("server-vp-1", 0, 1, 100, 1, 1, 2000), sizeRow("server-vp-2", 20), - ...createTableRows("server-vp-2", 0, 10), + ...createTableRows("server-vp-2", 0, 10, 100, 2, 0, 2000), ], }, }); @@ -2587,7 +2587,7 @@ describe("ServerProxy", () => { { mode: "update", rows: [ - [0,0,true,false,0,0,'key-00', 0,'key-00', 'name 00',1000,true], + [0,0,true,false,0,0,'key-00', 0,'key-00', 'name 00',2000,true], ], type: 'viewport-update', clientViewportId: 'client-vp-1' @@ -2598,16 +2598,16 @@ describe("ServerProxy", () => { { mode: "batch", rows: [ - [0,0,true,false,0,0,'key-00', 0,'key-00', 'name 00',1000,true], - [1,1,true,false,0,0,"key-01",0,"key-01","name 01",1001,true], - [2,2,true,false,0,0,"key-02",0,"key-02","name 02",1002,true], - [3,3,true,false,0,0,"key-03",0,"key-03","name 03",1003,true], - [4,4,true,false,0,0,"key-04",0,"key-04","name 04",1004,true], - [5,5,true,false,0,0,"key-05",0,"key-05","name 05",1005,true], - [6,6,true,false,0,0,"key-06",0,"key-06","name 06",1006,true], - [7,7,true,false,0,0,"key-07",0,"key-07","name 07",1007,true], - [8,8,true,false,0,0,"key-08",0,"key-08","name 08",1008,true], - [9,9,true,false,0,0,"key-09",0,"key-09","name 09",1009,true] + [0,0,true,false,0,0,'key-00', 0,'key-00', 'name 00',2000,true], + [1,1,true,false,0,0,"key-01",0,"key-01","name 01",2001,true], + [2,2,true,false,0,0,"key-02",0,"key-02","name 02",2002,true], + [3,3,true,false,0,0,"key-03",0,"key-03","name 03",2003,true], + [4,4,true,false,0,0,"key-04",0,"key-04","name 04",2004,true], + [5,5,true,false,0,0,"key-05",0,"key-05","name 05",2005,true], + [6,6,true,false,0,0,"key-06",0,"key-06","name 06",2006,true], + [7,7,true,false,0,0,"key-07",0,"key-07","name 07",2007,true], + [8,8,true,false,0,0,"key-08",0,"key-08","name 08",2008,true], + [9,9,true,false,0,0,"key-09",0,"key-09","name 09",2009,true] ], size: 100, type: 'viewport-update', diff --git a/vuu-ui/packages/vuu-data/test/test-utils.ts b/vuu-ui/packages/vuu-data/test/test-utils.ts index 44e2570fc..43a4d2e0c 100644 --- a/vuu-ui/packages/vuu-data/test/test-utils.ts +++ b/vuu-ui/packages/vuu-data/test/test-utils.ts @@ -46,14 +46,15 @@ export const createTableRows = ( to, vpSize = 100, ts = 1, - sel: 0 | 1 = 0 + sel: 0 | 1 = 0, + numericValue = 1000 ): VuuRow[] => { const results: VuuRow[] = []; for (let rowIndex = from; rowIndex < to; rowIndex++) { const key = ("0" + rowIndex).slice(-2); const rowKey = `key-${key}`; results.push({ - data: [rowKey, `name ${key}`, 1000 + rowIndex, true], + data: [rowKey, `name ${key}`, numericValue + rowIndex, true], rowIndex, rowKey, updateType: "U", diff --git a/vuu-ui/packages/vuu-datagrid/src/context-menu/useContextMenu.ts b/vuu-ui/packages/vuu-datagrid/src/context-menu/useContextMenu.ts index af6b3bcb6..ae21ba02a 100644 --- a/vuu-ui/packages/vuu-datagrid/src/context-menu/useContextMenu.ts +++ b/vuu-ui/packages/vuu-datagrid/src/context-menu/useContextMenu.ts @@ -2,8 +2,8 @@ import { DataSource } from "@finos/vuu-data"; import { DataSourceFilter, MenuActionHandler } from "@finos/vuu-data-types"; import { KeyedColumnDescriptor } from "@finos/vuu-datagrid-types"; +import { MenuActionClosePopup } from "@finos/vuu-popups"; import { removeColumnFromFilter, setAggregations } from "@finos/vuu-utils"; -import { MenuActionClosePopup } from "packages/vuu-popups/src"; import { AggregationType } from "../constants"; import { GridModelDispatch } from "../grid-context"; import { GridModelType } from "../grid-model/gridModelTypes"; diff --git a/vuu-ui/packages/vuu-filters/src/filter-builder-menu/FilterBuilderMenu.tsx b/vuu-ui/packages/vuu-filters/src/filter-builder-menu/FilterBuilderMenu.tsx index 338ef654a..5311cc38b 100644 --- a/vuu-ui/packages/vuu-filters/src/filter-builder-menu/FilterBuilderMenu.tsx +++ b/vuu-ui/packages/vuu-filters/src/filter-builder-menu/FilterBuilderMenu.tsx @@ -1,6 +1,6 @@ import { ContextMenuProps } from "@finos/vuu-popups"; import { MenuActionHandler } from "packages/vuu-data-types"; -import { ReactElement, useCallback, useEffect, useRef } from "react"; +import { ReactElement, useCallback, useRef } from "react"; import { PopupComponent as Popup, Portal } from "@finos/vuu-popups"; import { List, ListItem } from "@finos/vuu-ui-controls"; diff --git a/vuu-ui/packages/vuu-filters/src/filter-clause/useFilterClauseEditor.ts b/vuu-ui/packages/vuu-filters/src/filter-clause/useFilterClauseEditor.ts index 1579b8a80..558d98afe 100644 --- a/vuu-ui/packages/vuu-filters/src/filter-clause/useFilterClauseEditor.ts +++ b/vuu-ui/packages/vuu-filters/src/filter-clause/useFilterClauseEditor.ts @@ -35,15 +35,12 @@ const getFocusedField = () => const focusNextFocusableElement = (direction: "fwd" | "bwd" = "fwd") => { const activeField = getFocusedField(); - console.log(`activeField = ${activeField?.className}`); const filterClause = activeField?.closest(".vuuFilterClause"); if (filterClause?.lastChild === activeField) { requestAnimationFrame(() => { - console.log("enmd o the line, baby, wait, then try again"); focusNextFocusableElement(); }); } else { - console.log("go ahead and focus next field"); const nextField = direction === "fwd" ? (activeField.nextElementSibling as HTMLElement) @@ -107,7 +104,6 @@ const navigateToNextInputIfAtBoundary = ( const nextField = field.nextSibling as HTMLElement; const nextInput = nextField?.querySelector("input"); evt.preventDefault(); - console.log("%cfocus nextInput", "color:green;font-weight:bold"); nextInput?.focus(); requestAnimationFrame(() => { nextInput?.select(); @@ -163,7 +159,6 @@ export const useFilterClauseEditor = ({ ); const setOperator = useCallback((op) => { - console.log(`setOperator ${op}`); _setOperator(op); }, []); @@ -173,13 +168,15 @@ export const useFilterClauseEditor = ({ const handleSelectionChangeColumn = useCallback< SingleSelectionHandler - >((evt, column) => { - console.log(`handleSelectionChangeColumn ${column.name}`); - setSelectedColumn(column ?? undefined); - setOperator(undefined); - setValue(undefined); - focusNextElement(); - }, []); + >( + (evt, column) => { + setSelectedColumn(column ?? undefined); + setOperator(undefined); + setValue(undefined); + focusNextElement(); + }, + [setOperator] + ); const handleSelectionChangeOperator = useCallback( (evt, selected) => { @@ -193,7 +190,7 @@ export const useFilterClauseEditor = ({ ); } }, - [] + [setOperator] ); const handleChangeValue = useCallback( diff --git a/vuu-ui/packages/vuu-layout/src/drag-drop/Draggable.ts b/vuu-ui/packages/vuu-layout/src/drag-drop/Draggable.ts index f0f84b5d8..859abf781 100644 --- a/vuu-ui/packages/vuu-layout/src/drag-drop/Draggable.ts +++ b/vuu-ui/packages/vuu-layout/src/drag-drop/Draggable.ts @@ -2,7 +2,7 @@ import { rect } from "@finos/vuu-utils"; import { ReactElement } from "react"; import { LayoutModel } from "../layout-reducer"; import { findTarget, followPath, getProps } from "../utils"; -import { BoxModel, Measurements, Position } from "./BoxModel"; +import { BoxModel, Measurements } from "./BoxModel"; import { DragDropRect } from "./dragDropTypes"; import { DragState, IntrinsicSizes } from "./DragState"; import { DropTarget, identifyDropTarget } from "./DropTarget"; diff --git a/vuu-ui/packages/vuu-popups/src/dialog/Dialog.tsx b/vuu-ui/packages/vuu-popups/src/dialog/Dialog.tsx index f3f487a4f..5d4dff0ec 100644 --- a/vuu-ui/packages/vuu-popups/src/dialog/Dialog.tsx +++ b/vuu-ui/packages/vuu-popups/src/dialog/Dialog.tsx @@ -1,7 +1,7 @@ import { Scrim } from "@salt-ds/lab"; import cx from "classnames"; -import { HTMLAttributes, useCallback, useRef, useState } from "react"; -import { PortalDeprecated } from "../portal-deprecated"; +import { HTMLAttributes, useCallback, useRef } from "react"; +import { Portal } from "../portal"; import { DialogHeader } from "../dialog-header"; import "./Dialog.css"; @@ -24,30 +24,17 @@ export const Dialog = ({ ...props }: DialogProps) => { const root = useRef(null); - const [posX] = useState(0); - const [posY] = useState(0); const close = useCallback(() => { onClose?.(); }, [onClose]); - const handleRender = useCallback(() => { - // if (center && isOpen && root.current) { - // const { width, height } = root.current.getBoundingClientRect(); - // const { innerWidth, innerHeight } = window; - // const x = innerWidth / 2 - width / 2; - // const y = innerHeight / 2 - height / 2; - // setPosX(x); - // setPosY(y); - // } - }, []); - if (!isOpen) { return null; } return ( - +
- + ); }; diff --git a/vuu-ui/packages/vuu-popups/src/menu/ContextMenu.css b/vuu-ui/packages/vuu-popups/src/menu/ContextMenu.css deleted file mode 100644 index 6856ab946..000000000 --- a/vuu-ui/packages/vuu-popups/src/menu/ContextMenu.css +++ /dev/null @@ -1,7 +0,0 @@ -.vuuContextMenu { - border-radius:4px; - box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); - border-color: var(--vuuMenuList-borderColor, var(--salt-container-primary-borderColor)); - border-style: solid; - border-width: 1px; -} \ No newline at end of file diff --git a/vuu-ui/packages/vuu-popups/src/menu/ContextMenu.tsx b/vuu-ui/packages/vuu-popups/src/menu/ContextMenu.tsx index b65a1e078..6d5d2cdd3 100644 --- a/vuu-ui/packages/vuu-popups/src/menu/ContextMenu.tsx +++ b/vuu-ui/packages/vuu-popups/src/menu/ContextMenu.tsx @@ -1,16 +1,18 @@ import { useCallback, useRef } from "react"; -import { PortalDeprecated } from "../portal-deprecated"; import { MenuList, MenuListProps } from "./MenuList"; import { useCascade } from "./use-cascade"; -// import { useClickAway } from "./use-click-away"; import { useItemsWithIdsNext } from "./use-items-with-ids-next"; import { useId } from "@finos/vuu-layout"; import { PopupCloseCallback } from "../popup"; import { ContextMenuOptions } from "./useContextMenu"; - -import "./ContextMenu.css"; +import { + PopupComponent as Popup, + Portal, + PortalProps, +} from "@finos/vuu-popups"; export interface ContextMenuProps extends Omit { + PortalProps?: Partial; onClose?: PopupCloseCallback; position?: { x: number; y: number }; withPortal?: boolean; @@ -19,6 +21,7 @@ export interface ContextMenuProps extends Omit { const noop = () => undefined; export const ContextMenu = ({ + PortalProps, activatedByKeyboard, children: childrenProp, className, @@ -53,14 +56,19 @@ export const ContextMenu = ({ [actions, id, onClose] ); - const { closeMenu, listItemProps, openMenu, openMenus, handleRender } = - useCascade({ - // FIXME - id: `${id}`, - onActivate: handleActivate, - onMouseEnterItem: handleMouseEnterItem, - position, - }); + const { + closeMenu, + listItemProps, + openMenu: onOpenMenu, + openMenus, + handleRender, + } = useCascade({ + // FIXME + id: `${id}`, + onActivate: handleActivate, + onMouseEnterItem: handleMouseEnterItem, + position, + }); closeMenuRef.current = closeMenu; const handleCloseMenu = () => { @@ -87,28 +95,33 @@ export const ContextMenu = ({ <> {openMenus.map(({ id: menuId, left, top }, i, all) => { const childMenuId = getChildMenuId(i); - // TODO don't need the portal here, vuu popup service takes care of this return ( - - + - {menus[menuId]} - - + + {menus[menuId]} + + + ); })} diff --git a/vuu-ui/packages/vuu-popups/src/menu/MenuList.css b/vuu-ui/packages/vuu-popups/src/menu/MenuList.css index dcfc61a28..16c33a0d5 100644 --- a/vuu-ui/packages/vuu-popups/src/menu/MenuList.css +++ b/vuu-ui/packages/vuu-popups/src/menu/MenuList.css @@ -6,7 +6,7 @@ --context-menu-padding: var(--hw-list-item-padding, 0 6px); --context-menu-shadow: var(--hw-dialog-shadow, 0 6px 12px rgba(0, 0, 0, 0.175)); --focus-visible-border-color: var(--hw-focus-visible-border-color, rgb(141, 154, 179)); - --context-menu-highlight-bg: var(--vuu-color-gray-10); + --context-menu-highlight-bg: var(--salt-selectable-background-hover); --context-menu-blur-focus-bg: #e0e4e9; --menu-item-icon-color: black; --menu-item-twisty-color: black; diff --git a/vuu-ui/packages/vuu-popups/src/menu/MenuList.tsx b/vuu-ui/packages/vuu-popups/src/menu/MenuList.tsx index bf4224993..08eb66c41 100644 --- a/vuu-ui/packages/vuu-popups/src/menu/MenuList.tsx +++ b/vuu-ui/packages/vuu-popups/src/menu/MenuList.tsx @@ -35,7 +35,12 @@ export interface MenuItemProps extends HTMLAttributes { // Purely used as markers, props will be extracted export const MenuItemGroup: FC = () => null; // eslint-disable-next-line no-unused-vars -export const MenuItem = ({ children, idx, ...props }: MenuItemProps) => { +export const MenuItem = ({ + children, + idx, + options, + ...props +}: MenuItemProps) => { return
{children}
; }; @@ -62,6 +67,10 @@ export const isMenuItemLabel = ( const hasIcon = (child: ReactElement) => child.props["data-icon"]; +export type MenuOpenHandler = ( + menuItemEl: HTMLElement, + immediate?: boolean +) => void; export interface MenuListProps extends HTMLAttributes { activatedByKeyboard?: boolean; children: ReactElement[]; @@ -72,7 +81,7 @@ export interface MenuListProps extends HTMLAttributes { listItemProps?: Partial; onActivate?: (menuId: string) => void; onCloseMenu: (idx: number) => void; - onOpenMenu?: (menuItemEl: HTMLElement) => void; + openMenu?: MenuOpenHandler; onHighlightMenuItem?: (idx: number) => void; } @@ -89,7 +98,7 @@ export const MenuList = ({ onHighlightMenuItem, onActivate, onCloseMenu, - onOpenMenu, + openMenu: onOpenMenu, ...props }: MenuListProps) => { const id = useId(idProp); @@ -99,7 +108,7 @@ export const MenuList = ({ const mapIdxToId = useMemo(() => new Map(), []); const handleActivate = (idx: number) => { - const el = root.current?.querySelector(`:scope > [data-idx='${idx}']`); + const el = root.current?.querySelector(`:scope > [data-index='${idx}']`); el?.id && onActivate?.(el.id); }; @@ -232,7 +241,7 @@ const getMenuItemProps = ( ) => ({ id: `menuitem-${itemId}`, key: key ?? idx, - "data-idx": idx, + "data-index": idx, "data-highlighted": idx === highlightedIdx || undefined, className: cx("vuuMenuItem", className, { "vuuMenuItem-separator": hasSeparator, diff --git a/vuu-ui/packages/vuu-popups/src/menu/list-dom-utils.ts b/vuu-ui/packages/vuu-popups/src/menu/list-dom-utils.ts index e34cd4157..dbcacfcc0 100644 --- a/vuu-ui/packages/vuu-popups/src/menu/list-dom-utils.ts +++ b/vuu-ui/packages/vuu-popups/src/menu/list-dom-utils.ts @@ -1,9 +1,6 @@ -// const listItemElement = (listEl: HTMLElement, listItemIdx: number) => -// listEl.querySelector(`:scope > [data-idx="${listItemIdx}"]`); - export function listItemIndex(listItemEl: HTMLElement) { if (listItemEl) { - const idx = listItemEl.dataset.idx; + const idx = listItemEl.dataset.index; if (idx) { return parseInt(idx, 10); // eslint-disable-next-line no-cond-assign @@ -16,7 +13,7 @@ export function listItemIndex(listItemEl: HTMLElement) { const listItemId = (el: HTMLElement | null | undefined) => el?.id; export const closestListItem = (el: HTMLElement | null | undefined) => - el?.closest("[data-idx],[aria-posinset]") as HTMLElement; + el?.closest("[data-index],[aria-posinset]") as HTMLElement; export const closestListItemId = (el: HTMLElement) => listItemId(closestListItem(el)); diff --git a/vuu-ui/packages/vuu-popups/src/menu/use-cascade.ts b/vuu-ui/packages/vuu-popups/src/menu/use-cascade.ts index 83ec46116..c726c2923 100644 --- a/vuu-ui/packages/vuu-popups/src/menu/use-cascade.ts +++ b/vuu-ui/packages/vuu-popups/src/menu/use-cascade.ts @@ -8,7 +8,7 @@ import { } from "react"; import { closestListItem } from "./list-dom-utils"; -import { MenuItemProps } from "./MenuList"; +import { MenuItemProps, MenuOpenHandler } from "./MenuList"; // import {mousePosition} from './aim/utils'; // import {aiming} from './aim/aim'; @@ -65,9 +65,15 @@ export type RuntimeMenuDescriptor = { top: number; }; +/** menuitem-vuu-1-0 vuu-1 */ export const getHostMenuId = (id: string, rootId: string) => { + console.log(`getHostMenuId from ${id} and ${rootId}`); const pos = id.lastIndexOf("-"); - return pos > -1 ? id.slice(9, pos) : rootId; + if (id.startsWith("menuitem")) { + return pos > -1 ? id.slice(9, pos) : rootId; + } else { + return pos > -1 ? id.slice(0, pos) : rootId; + } }; const getTargetMenuId = (id: string) => id.slice(9); @@ -100,7 +106,7 @@ export interface CascadeHooksResult { closeMenu: () => void; handleRender: () => void; listItemProps: Partial; - openMenu: (menuItemEl: HTMLElement) => void; + openMenu: MenuOpenHandler; openMenus: RuntimeMenuDescriptor[]; } @@ -133,6 +139,9 @@ export const useCascade = ({ }, []); const setOpenMenus = useCallback((menus: RuntimeMenuDescriptor[]) => { + console.log(`setOpenMenus`, { + menus, + }); openMenus.current = menus; forceRefresh({}); }, []); @@ -146,9 +155,13 @@ export const useCascade = ({ const openMenu = useCallback( (hostMenuId = rootId, targetMenuId: string, itemId = null) => { + console.log( + `open menu hostMenuId ${hostMenuId} targetMenuId ${targetMenuId} itemId ${itemId}` + ); if (hostMenuId === rootId && itemId === null) { setOpenMenus([{ id: rootId, left: posX, top: posY }]); } else { + console.log(`openMenu set ${hostMenuId} status to popup-open`); menuState.current[hostMenuId] = "popup-open"; const el = document.getElementById(itemId) as HTMLElement; if (el !== null) { @@ -166,7 +179,9 @@ export const useCascade = ({ const closeMenu = useCallback( (menuId?: string) => { + console.log(`closeMenu ${menuId}`); if (menuId === rootId) { + console.log("close child menu of root"); setOpenMenus([]); } else { const menus = openMenus.current.slice(); @@ -176,6 +191,9 @@ export const useCascade = ({ if (parentMenu) { menuState.current[parentMenu.id] = "no-popup"; } + console.log(`closeMenu setOpenMenus`, { + menus, + }); setOpenMenus(menus); } }, @@ -184,17 +202,27 @@ export const useCascade = ({ const closeMenus = useCallback( (menuItemId) => { + console.log(`closeMenus ${menuItemId}`); const menus = openMenus.current.slice(); const menuItemMenuId = menuItemId.slice(9); let { id: lastMenuId } = menus.at(-1) as RuntimeMenuDescriptor; while (menus.length > 1 && !menuItemMenuId.startsWith(lastMenuId)) { const parentMenuId = getHostMenuId(lastMenuId, rootId); + console.log( + `parentMenuId of lastMenuId ${lastMenuId} and rootId ${rootId} is ${parentMenuId}` + ); menus.pop(); + console.log( + `set state to no-popup for ${lastMenuId} and ${parentMenuId}` + ); menuState.current[lastMenuId] = "no-popup"; menuState.current[parentMenuId] = "no-popup"; ({ id: lastMenuId } = menus[menus.length - 1]); } if (menus.length < openMenus.current.length) { + console.log(`closeMenus setOpenMenus`, { + menus, + }); setOpenMenus(menus); } }, @@ -209,7 +237,12 @@ export const useCascade = ({ }, []); const scheduleOpen = useCallback( - (hostMenuId: string, targetMenuId: string, menuItemId: string) => { + ( + hostMenuId: string, + targetMenuId: string, + menuItemId: string, + delay = 300 + ) => { clearAnyScheduledOpenTasks(); // do we need to set target state to pending-open ?s @@ -221,7 +254,7 @@ export const useCascade = ({ menuState.current[hostMenuId] = "popup-open"; menuState.current[targetMenuId] = "no-popup"; openMenu(hostMenuId, targetMenuId, menuItemId); - }, 400); + }, delay); }, [clearAnyScheduledOpenTasks, closeMenus, openMenu] ); @@ -233,6 +266,7 @@ export const useCascade = ({ // ); menuState.current[openMenuId] = "pending-close"; menuClosePendingTimeout.current = window.setTimeout(() => { + // console.log(`call closeMenus from scheduleClose`); closeMenus(itemId); }, 400); }, @@ -241,8 +275,8 @@ export const useCascade = ({ const handleRender = useCallback(() => { const { current: menus } = openMenus; - const [menu] = menus.slice(-1); - const el = document.getElementById(menu.id); + const menu = menus.at(-1); + const el = menu ? document.getElementById(menu.id) : undefined; if (el) { const { right, bottom } = el.getBoundingClientRect(); const { clientHeight, clientWidth } = document.body; @@ -261,22 +295,24 @@ export const useCascade = ({ el.focus(); } } else { - console.log(`no element found with if ${menu.id}`); + console.log(`useCascade no element found with if ${menu?.id}`); } }, [rootId, setOpenMenus]); // TODO introduce a delay parameter that allows click to requeat an immediate render - const triggerChildMenu = useCallback( - (menuItemEl: HTMLElement) => { + const triggerChildMenu = useCallback( + (menuItemEl, immediate = false) => { const { hostMenuId, targetMenuId, menuItemId, isGroup, isOpen } = getMenuItemDetails(menuItemEl, rootId); - const { current: { [hostMenuId]: state }, } = menuState; + const delay = immediate ? 0 : undefined; + // console.log( // `%ctriggerChildMenu + // rootId ${rootId} // menuItem ${menuItemId} // host menu: ${hostMenuId} // target menu: ${targetMenuId} @@ -290,14 +326,14 @@ export const useCascade = ({ if (state === "no-popup" && isGroup) { menuState.current[hostMenuId] = "popup-pending"; - scheduleOpen(hostMenuId, targetMenuId, menuItemId); + scheduleOpen(hostMenuId, targetMenuId, menuItemId, delay); } else if (state === "popup-pending" && !isGroup) { menuState.current[hostMenuId] = "no-popup"; clearTimeout(menuOpenPendingTimeout.current); menuOpenPendingTimeout.current = undefined; } else if (state === "popup-pending" && isGroup) { clearTimeout(menuOpenPendingTimeout.current); - scheduleOpen(hostMenuId, targetMenuId, menuItemId); + scheduleOpen(hostMenuId, targetMenuId, menuItemId, delay); } else if (state === "popup-open") { if (menuIsOpen(targetMenuId)) { const menuStatus = getOpenMenuStatus(targetMenuId); @@ -320,6 +356,11 @@ export const useCascade = ({ // TODO review the below, suspectb it's over complicating things const [parentOfLastOpenedMenu, lastOpenedMenu] = openMenus.current.slice(-2); + console.log(`about to check id on `, { + openMenus, + parentOfLastOpenedMenu, + lastOpenedMenu, + }); if ( parentOfLastOpenedMenu.id === hostMenuId && menuState.current[lastOpenedMenu.id] !== "pending-close" /*&& @@ -327,7 +368,7 @@ export const useCascade = ({ ) { scheduleClose(hostMenuId, lastOpenedMenu.id, menuItemId); if (isGroup && !isOpen) { - scheduleOpen(hostMenuId, targetMenuId, menuItemId); + scheduleOpen(hostMenuId, targetMenuId, menuItemId, delay); } } else if ( parentOfLastOpenedMenu.id === hostMenuId && @@ -336,10 +377,10 @@ export const useCascade = ({ menuState.current[lastOpenedMenu.id] === "pending-close" ) { // if there is already an item queued for opening cancel it - scheduleOpen(hostMenuId, targetMenuId, menuItemId); + scheduleOpen(hostMenuId, targetMenuId, menuItemId, delay); } else if (isGroup) { // closeMenus(menuId, itemId); - scheduleOpen(hostMenuId, targetMenuId, menuItemId); + scheduleOpen(hostMenuId, targetMenuId, menuItemId, delay); } else if ( !( (menuState.current[lastOpenedMenu.id] === "pending-close") /*&& @@ -373,11 +414,13 @@ export const useCascade = ({ () => ({ onMouseEnter: (evt: MouseEvent) => { const menuItemEl = closestListItem(evt.target as HTMLElement); + console.log(`onMouseEnter ${menuItemEl?.id}`); triggerChildMenu(menuItemEl); onMouseEnterItem(evt, menuItemEl.id); }, onClick: (evt: SyntheticEvent) => { + console.log("click"); const listItemEl = closestListItem(evt.target as HTMLElement); const { isGroup, menuItemId } = getMenuItemDetails(listItemEl, rootId); if (isGroup) { diff --git a/vuu-ui/packages/vuu-popups/src/menu/use-keyboard-navigation.ts b/vuu-ui/packages/vuu-popups/src/menu/use-keyboard-navigation.ts index 5cd06a0bd..3d2ca4b7c 100644 --- a/vuu-ui/packages/vuu-popups/src/menu/use-keyboard-navigation.ts +++ b/vuu-ui/packages/vuu-popups/src/menu/use-keyboard-navigation.ts @@ -9,6 +9,7 @@ import { import { hasPopup, isRoot } from "./utils"; import { isNavigationKey } from "./key-code"; import { isValidNumber } from "@finos/vuu-utils"; +import { MenuOpenHandler } from "./MenuList"; export interface KeyboardNavigationProps { autoHighlightFirstItem?: boolean; @@ -18,7 +19,7 @@ export interface KeyboardNavigationProps { onActivate: (idx: number) => void; onHighlight?: (idx: number) => void; onCloseMenu: (idx: number) => void; - onOpenMenu?: (menuItemEl: HTMLElement) => void; + onOpenMenu?: MenuOpenHandler; } export interface KeyboardHookListProps { @@ -121,11 +122,11 @@ export const useKeyboardNavigation = ({ ) { const menuEl = e.target as HTMLElement; const menuItemEl = menuEl.querySelector( - `:scope > [data-idx='${highlightedIndex}']` + `:scope > [data-index='${highlightedIndex}']` ) as HTMLElement; if (menuItemEl) { - onOpenMenu?.(menuItemEl); + onOpenMenu?.(menuItemEl, true); } } else if (e.key === "ArrowLeft" && !isRoot(e.target as HTMLElement)) { onCloseMenu(highlightedIndex); diff --git a/vuu-ui/packages/vuu-popups/src/menu/useContextMenu.tsx b/vuu-ui/packages/vuu-popups/src/menu/useContextMenu.tsx index 17edd2728..54e1355a7 100644 --- a/vuu-ui/packages/vuu-popups/src/menu/useContextMenu.tsx +++ b/vuu-ui/packages/vuu-popups/src/menu/useContextMenu.tsx @@ -3,10 +3,8 @@ import { MenuActionHandler, MenuBuilder, } from "@finos/vuu-data-types"; -import { useThemeAttributes } from "@finos/vuu-shell"; import { isGroupMenuItemDescriptor } from "@finos/vuu-utils"; -import cx from "classnames"; -import { cloneElement, useCallback, useContext } from "react"; +import { cloneElement, useCallback, useContext, useMemo } from "react"; import { MenuActionClosePopup, PopupCloseReason, @@ -16,13 +14,13 @@ import { import { ContextMenu, ContextMenuProps } from "./ContextMenu"; import { MenuItem, MenuItemGroup } from "./MenuList"; import { ContextMenuContext } from "./context-menu-provider"; +import { useThemeAttributes } from "@finos/vuu-shell"; export type ContextMenuOptions = { [key: string]: unknown; contextMenu?: JSX.Element; ContextMenuProps?: Partial & { className?: string; - "data-mode"?: string; }; controlledComponentId?: string; }; @@ -46,7 +44,16 @@ export const useContextMenu = ( menuActionHandler?: MenuActionHandler ): [ShowContextMenu, () => void] => { const ctx = useContext(ContextMenuContext); + const [themeClass, densityClass, dataMode] = useThemeAttributes(); + const themeAttributes = useMemo( + () => ({ + themeClass, + densityClass, + dataMode, + }), + [dataMode, densityClass, themeClass] + ); const buildMenuOptions = useCallback( (menuBuilders: MenuBuilder[], location, options) => { @@ -106,14 +113,13 @@ export const useContextMenu = ( }; if (menuItemDescriptors.length && menuHandler) { + // because showPopup is going to be used to render the context menu, it will not + // have access to the ContextMenuContext. Pass the theme attributes here showContextMenu(e, menuItemDescriptors, menuHandler, { + PortalProps: { + themeAttributes, + }, ...ContextMenuProps, - className: cx( - ContextMenuProps?.className, - themeClass, - densityClass - ), - "data-mode": dataMode, }); } } else { @@ -122,19 +128,11 @@ export const useContextMenu = ( ); } }, - [ - buildMenuOptions, - ctx, - dataMode, - densityClass, - menuActionHandler, - menuBuilder, - themeClass, - ] + [buildMenuOptions, ctx, menuActionHandler, menuBuilder, themeAttributes] ); const hideContextMenu = useCallback(() => { - console.log("hide comnytext menu"); + console.log("hide context menu"); }, []); return [handleShowContextMenu, hideContextMenu]; @@ -202,7 +200,6 @@ const showContextMenu = ( const component = ( diff --git a/vuu-ui/packages/vuu-popups/src/menu/utils.ts b/vuu-ui/packages/vuu-popups/src/menu/utils.ts index ee8b35a24..ced575be1 100644 --- a/vuu-ui/packages/vuu-popups/src/menu/utils.ts +++ b/vuu-ui/packages/vuu-popups/src/menu/utils.ts @@ -3,5 +3,5 @@ export const isRoot = (el: HTMLElement) => export const hasPopup = (el: HTMLElement, idx: number) => (el.ariaHasPopup === "true" && el.dataset?.idx === `${idx}`) || - el.querySelector(`:scope > [data-idx='${idx}'][aria-haspopup='true']`) !== + el.querySelector(`:scope > [data-index='${idx}'][aria-haspopup='true']`) !== null; diff --git a/vuu-ui/packages/vuu-popups/src/popup-menu/PopupMenu.tsx b/vuu-ui/packages/vuu-popups/src/popup-menu/PopupMenu.tsx index a36efbddd..17510290c 100644 --- a/vuu-ui/packages/vuu-popups/src/popup-menu/PopupMenu.tsx +++ b/vuu-ui/packages/vuu-popups/src/popup-menu/PopupMenu.tsx @@ -6,6 +6,7 @@ import { useState, } from "react"; import { + MenuOpenHandler, PopupCloseReason, reasonIsClickAway, useContextMenu, @@ -13,9 +14,9 @@ import { import cx from "classnames"; import { Button } from "@salt-ds/core"; import { useId } from "@finos/vuu-layout"; +import { MenuActionHandler, MenuBuilder } from "@finos/vuu-data-types"; import "./PopupMenu.css"; -import { MenuActionHandler, MenuBuilder } from "@finos/vuu-data-types"; const classBase = "vuuPopupMenu"; @@ -55,6 +56,12 @@ export const PopupMenu = ({ const id = useId(idProp); const [showContextMenu] = useContextMenu(menuBuilder, menuActionHandler); + const handleOpenMenu = useCallback((el) => { + console.log(`menu Open `, { + el, + }); + }, []); + const handleMenuClose = useCallback( (reason?: PopupCloseReason) => { setMenuOpen(false); @@ -86,16 +93,23 @@ export const PopupMenu = ({ setMenuOpen(true); showContextMenu(e, menuLocation, { ContextMenuProps: { - className: "vuuPopupMenuList", id: `${id}-menu`, onClose: handleMenuClose, + openMenu: handleOpenMenu, position: getPosition(rootRef.current), }, ...menuOptions, }); } }, - [handleMenuClose, id, menuLocation, menuOptions, showContextMenu] + [ + handleMenuClose, + handleOpenMenu, + id, + menuLocation, + menuOptions, + showContextMenu, + ] ); return ( diff --git a/vuu-ui/packages/vuu-popups/src/popup/Popup.tsx b/vuu-ui/packages/vuu-popups/src/popup/Popup.tsx index 312ff66f7..118436779 100644 --- a/vuu-ui/packages/vuu-popups/src/popup/Popup.tsx +++ b/vuu-ui/packages/vuu-popups/src/popup/Popup.tsx @@ -1,11 +1,12 @@ import cx from "classnames"; import { useThemeAttributes } from "@finos/vuu-shell"; import { HTMLAttributes, RefObject } from "react"; -import { useAnchoredPosition } from "./useAnchoredPosition"; +import { Position, useAnchoredPosition } from "./useAnchoredPosition"; import "./Popup.css"; export type PopupPlacement = + | "absolute" | "below" | "below-center" | "below-full-width" @@ -13,11 +14,12 @@ export type PopupPlacement = | "right"; export interface PopupComponentProps extends HTMLAttributes { - placement: PopupPlacement; anchorElement: RefObject; minWidth?: number; offsetLeft?: number; offsetTop?: number; + placement: PopupPlacement; + position?: Position; } export const PopupComponent = ({ @@ -26,20 +28,16 @@ export const PopupComponent = ({ anchorElement, minWidth, placement, + position: positionProp, }: PopupComponentProps) => { - const [themeClass, densityClass, dataMode] = useThemeAttributes(); const { popupRef, position } = useAnchoredPosition({ anchorElement, minWidth, placement, + position: positionProp, }); return position === undefined ? null : ( -
+
{children}
); diff --git a/vuu-ui/packages/vuu-popups/src/popup/popup-service.ts b/vuu-ui/packages/vuu-popups/src/popup/popup-service.ts index 600fbf888..34ede5088 100644 --- a/vuu-ui/packages/vuu-popups/src/popup/popup-service.ts +++ b/vuu-ui/packages/vuu-popups/src/popup/popup-service.ts @@ -1,12 +1,9 @@ import cx from "classnames"; -import { escape } from "querystring"; import React, { createElement, CSSProperties, HTMLAttributes, ReactElement, - useEffect, - useRef, } from "react"; import ReactDOM from "react-dom"; import { ContextMenuOptions } from "../menu"; @@ -63,7 +60,9 @@ function specialKeyHandler(e: KeyboardEvent) { function outsideClickHandler(e: MouseEvent) { if (_popups.length) { - const popupContainers = document.body.querySelectorAll(".vuuPopup"); + const popupContainers = document.body.querySelectorAll( + ".vuuPopup,#vuu-portal-root" + ); for (let i = 0; i < popupContainers.length; i++) { if (popupContainers[i].contains(e.target as HTMLElement)) { return; @@ -293,78 +292,3 @@ export class DialogService { } } } - -export interface PopupProps { - children: ReactElement; - close?: boolean; - depth: number; - group?: string; - name: string; - position?: "above" | "below" | ""; - width: number; -} - -export const Popup = (props: PopupProps) => { - const pendingTask = useRef(); - const ref = useRef(null); - - const show = (props: PopupProps, boundingClientRect: DOMRect) => { - const { name, group, depth, width } = props; - let left: number | undefined; - let top: number | undefined; - - if (pendingTask.current) { - window.clearTimeout(pendingTask.current); - pendingTask.current = undefined; - } - - if (props.close === true) { - PopupService.hidePopup(undefined, name, group); - } else { - const { position, children: component } = props; - const { - left: targetLeft, - top: targetTop, - width: clientWidth, - bottom: targetBottom, - } = boundingClientRect; - - if (position === "below") { - left = targetLeft; - top = targetBottom; - } else if (position === "above") { - left = targetLeft; - top = targetTop; - } - - pendingTask.current = window.setTimeout(() => { - PopupService.showPopup({ - name, - group, - depth, - position, - left, - top, - width: width || clientWidth, - component, - }); - }, 10); - } - }; - - useEffect(() => { - if (ref.current) { - const el = ref.current.parentElement; - const boundingClientRect = el?.getBoundingClientRect(); - if (boundingClientRect) { - show(props, boundingClientRect); - } - } - - return () => { - PopupService.hidePopup(undefined, props.name, props.group); - }; - }, [props]); - - return React.createElement("div", { className: "popup-proxy", ref }); -}; diff --git a/vuu-ui/packages/vuu-popups/src/popup/useAnchoredPosition.ts b/vuu-ui/packages/vuu-popups/src/popup/useAnchoredPosition.ts index 7e416bab7..f6fa8f635 100644 --- a/vuu-ui/packages/vuu-popups/src/popup/useAnchoredPosition.ts +++ b/vuu-ui/packages/vuu-popups/src/popup/useAnchoredPosition.ts @@ -1,19 +1,15 @@ -import { - RefObject, - useCallback, - useLayoutEffect, - useRef, - useState, -} from "react"; -import { PopupPlacement } from "./Popup"; +import { useCallback, useLayoutEffect, useRef, useState } from "react"; +import { PopupComponentProps, PopupPlacement } from "./Popup"; -export interface AnchoredPositionHookProps { - anchorElement: RefObject; - minWidth?: number; - offsetLeft?: number; - offsetTop?: number; - placement: PopupPlacement; -} +export type AnchoredPositionHookProps = Pick< + PopupComponentProps, + | "anchorElement" + | "minWidth" + | "offsetLeft" + | "offsetTop" + | "placement" + | "position" +>; export type Visibility = "hidden" | "visible"; @@ -82,13 +78,16 @@ export const useAnchoredPosition = ({ offsetLeft = 0, offsetTop = 0, placement, + position: positionProp, }: AnchoredPositionHookProps) => { const popupRef = useRef(null); - const [position, setPosition] = useState(); + const [position, setPosition] = useState(positionProp); // maybe better as useMemo ? useLayoutEffect(() => { - if (anchorElement.current) { + if (placement === "absolute" && positionProp) { + setPosition(positionProp); + } else if (anchorElement.current) { const dimensions = popupRef.current === null ? undefined @@ -103,7 +102,7 @@ export const useAnchoredPosition = ({ ); setPosition(position); } - }, [anchorElement, minWidth, offsetLeft, offsetTop, placement]); + }, [anchorElement, minWidth, offsetLeft, offsetTop, placement, positionProp]); const popupCallbackRef = useCallback( (el: HTMLDivElement | null) => { diff --git a/vuu-ui/packages/vuu-popups/src/portal-deprecated/PortalDeprecated.tsx b/vuu-ui/packages/vuu-popups/src/portal-deprecated/PortalDeprecated.tsx deleted file mode 100644 index 098eb1452..000000000 --- a/vuu-ui/packages/vuu-popups/src/portal-deprecated/PortalDeprecated.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { ReactElement, useLayoutEffect, useMemo } from "react"; -import * as ReactDOM from "react-dom"; -import { createContainer, renderPortal } from "./render-portal"; -import { useThemeAttributes } from "@finos/vuu-shell"; -import cx from "classnames"; - -export interface PortalDeprecatedProps { - children: ReactElement; - onRender?: () => void; - x?: number; - y?: number; -} - -export const PortalDeprecated = function Portal({ - children, - x = 0, - y = 0, - onRender, -}: PortalDeprecatedProps) { - // Do we need to accept container here as a prop ? - const [themeClass, densityClass, dataMode] = useThemeAttributes(); - const renderContainer = useMemo(() => { - return createContainer({ - className: cx(themeClass, densityClass), - dataMode, - }); - }, [dataMode, densityClass, themeClass]); - - useLayoutEffect(() => { - renderPortal(children, renderContainer, x, y, onRender); - }, [children, onRender, renderContainer, x, y]); - - useLayoutEffect(() => { - return () => { - if (renderContainer) { - ReactDOM.unmountComponentAtNode(renderContainer); - if (renderContainer.classList.contains("vuuPopup")) { - renderContainer.parentElement?.removeChild(renderContainer); - } - } - }; - }, [renderContainer]); - - // useLayoutEffect(() => { - // renderContainer.current = renderPortal(children, x, y, container) - // return () => { - // if (renderContainer.current){ - // console.log('EXPLICIT UNMOUNT') - // ReactDOM.unmountComponentAtNode(renderContainer.current); - // if (renderContainer.current.classList.contains('hwReactPopup')){ - // renderContainer.current.parentElement.removeChild(renderContainer.current); - // renderContainer.current = null; - // } - // } - // } - // },[]) - return null; -}; diff --git a/vuu-ui/packages/vuu-popups/src/portal-deprecated/index.ts b/vuu-ui/packages/vuu-popups/src/portal-deprecated/index.ts index abef1b876..87b43b8b9 100644 --- a/vuu-ui/packages/vuu-popups/src/portal-deprecated/index.ts +++ b/vuu-ui/packages/vuu-popups/src/portal-deprecated/index.ts @@ -1,3 +1 @@ -export * from "./PortalDeprecated"; export * from "./render-portal"; -export * from "./portal-utils"; diff --git a/vuu-ui/packages/vuu-popups/src/portal-deprecated/portal-utils.ts b/vuu-ui/packages/vuu-popups/src/portal-deprecated/portal-utils.ts deleted file mode 100644 index eae63e58b..000000000 --- a/vuu-ui/packages/vuu-popups/src/portal-deprecated/portal-utils.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const installTheme = (themeId: string) => { - const installedThemes = getComputedStyle(document.body).getPropertyValue( - "--installed-themes" - ); - document.body.style.setProperty( - "--installed-themes", - `${installedThemes} ${themeId}` - ); -}; diff --git a/vuu-ui/packages/vuu-popups/src/portal/Portal.css b/vuu-ui/packages/vuu-popups/src/portal/Portal.css index d14e21ac1..787edc4ae 100644 --- a/vuu-ui/packages/vuu-popups/src/portal/Portal.css +++ b/vuu-ui/packages/vuu-popups/src/portal/Portal.css @@ -10,16 +10,6 @@ z-index: var(--salt-zIndex-modal); } -.vuuPopupMenuList { - border-radius:4px; - box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); - position: absolute; - border-color: var(--vuuMenuList-borderColor, var(--salt-container-primary-borderColor)); - border-style: solid !important; - border-width: 1px; - padding: 4px 0; -} - .vuuPortal:has(.vuuDropdown-popup-component.vuuList-empty){ display: none; diff --git a/vuu-ui/packages/vuu-popups/src/portal/Portal.tsx b/vuu-ui/packages/vuu-popups/src/portal/Portal.tsx index 27fb38a94..f09bd0045 100644 --- a/vuu-ui/packages/vuu-popups/src/portal/Portal.tsx +++ b/vuu-ui/packages/vuu-popups/src/portal/Portal.tsx @@ -1,4 +1,4 @@ -import { useThemeAttributes } from "@finos/vuu-shell"; +import { ThemeAttributes, useThemeAttributes } from "@finos/vuu-shell"; import { ReactNode, useLayoutEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; @@ -21,11 +21,21 @@ export interface PortalProps { * If this node does not exist on the document, it will be created for you. */ id?: string; + /** + * Callback invoked immediately after render (in layoutEffect). Can be + * used to check position vis-a-vis viewport and adjust if out of bounds + */ + onRender?: () => void; /** * Allow conditional rendering of this Portal, if false, will render nothing. * Defaults to true */ open?: boolean; + /** + * ThemeAttributes can be passed in for cases where ContextMenu is + * rendered via popup-service showPopup, outside the Context hierarchy. + */ + themeAttributes?: ThemeAttributes; } function getContainer(container: PortalProps["container"]) { @@ -42,12 +52,15 @@ export const Portal = ({ children, container: containerProp = document.body, id = DEFAULT_ID, + onRender, open = true, + themeAttributes, }: PortalProps) => { const [mounted, setMounted] = useState(false); const portalRef = useRef(null); const container = getContainer(containerProp) ?? document.body; - const [themeClass, densityClass, dataMode] = useThemeAttributes(); + const [themeClass, densityClass, dataMode] = + useThemeAttributes(themeAttributes); useLayoutEffect(() => { const root = document.getElementById(id); @@ -66,6 +79,12 @@ export const Portal = ({ setMounted(true); }, [id, container, themeClass, densityClass, dataMode]); + useLayoutEffect(() => { + requestAnimationFrame(() => { + onRender?.(); + }); + }, [onRender]); + if (open && mounted && portalRef.current && children) { return createPortal(children, portalRef.current); } diff --git a/vuu-ui/packages/vuu-shell/src/theme-provider/ThemeProvider.tsx b/vuu-ui/packages/vuu-shell/src/theme-provider/ThemeProvider.tsx index a9020cb3a..660e95bc7 100644 --- a/vuu-ui/packages/vuu-shell/src/theme-provider/ThemeProvider.tsx +++ b/vuu-ui/packages/vuu-shell/src/theme-provider/ThemeProvider.tsx @@ -28,17 +28,31 @@ export const ThemeContext = createContext({ themeMode: "light", }); -export type ThemeClasses = [string, string, string]; +export type ThemeClasses = [string, string, ThemeMode]; const DEFAULT_THEME_ATTRIBUTES: ThemeClasses = [ "vuu", "vuu-density-high", - "light", + "light" as ThemeMode, ]; -export const useThemeAttributes = (): [string, string, string] => { +export type ThemeAttributes = { + themeClass: string; + densityClass: string; + dataMode: ThemeMode; +}; + +export const useThemeAttributes = ( + themeAttributes?: ThemeAttributes +): [string, string, ThemeMode] => { const context = useContext(ThemeContext); - if (context) { + if (themeAttributes) { + return [ + themeAttributes.themeClass, + themeAttributes.densityClass, + themeAttributes.dataMode, + ]; + } else if (context) { return [ `${context.theme}-theme`, `${context.theme}-density-${context.density}`, diff --git a/vuu-ui/packages/vuu-table/src/table-next/column-menu/ColumnMenu.css b/vuu-ui/packages/vuu-table/src/table-next/column-menu/ColumnMenu.css index adcd4565a..a42cef5d2 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/column-menu/ColumnMenu.css +++ b/vuu-ui/packages/vuu-table/src/table-next/column-menu/ColumnMenu.css @@ -35,12 +35,4 @@ --vuu-icon-size: 14px; } - .vuuColumnMenuList { - --vuuMenuList-borderStyle: solid; - border-radius:4px; - box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); - border-color: var(--vuuMenuList-borderColor, var(--salt-container-primary-borderColor)); - border-style: solid; - border-width: 1px; - } \ No newline at end of file diff --git a/vuu-ui/packages/vuu-table/src/table-next/column-menu/ColumnMenu.tsx b/vuu-ui/packages/vuu-table/src/table-next/column-menu/ColumnMenu.tsx index c6058e2a8..dfc0e6cf6 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/column-menu/ColumnMenu.tsx +++ b/vuu-ui/packages/vuu-table/src/table-next/column-menu/ColumnMenu.tsx @@ -41,7 +41,6 @@ export const ColumnMenu = ({ showContextMenu(e, "column-menu", { column, ContextMenuProps: { - className: "vuuColumnMenuList", onClose: handleMenuClose, position: getPosition(rootRef.current), }, diff --git a/vuu-ui/packages/vuu-table/src/table/dataTableTypes.ts b/vuu-ui/packages/vuu-table/src/table/dataTableTypes.ts index 68c3dd7ca..2db9f107e 100644 --- a/vuu-ui/packages/vuu-table/src/table/dataTableTypes.ts +++ b/vuu-ui/packages/vuu-table/src/table/dataTableTypes.ts @@ -12,7 +12,7 @@ import { TableHeadings, TableSelectionModel, } from "@finos/vuu-datagrid-types"; -import { VuuDataRow } from "packages/vuu-protocol-types"; +import { VuuDataRow } from "@finos/vuu-protocol-types"; import { FC, HTMLAttributes, MouseEvent } from "react"; import { RowProps } from "../table-next/Row"; diff --git a/vuu-ui/packages/vuu-table/src/table/useSelection.ts b/vuu-ui/packages/vuu-table/src/table/useSelection.ts index 13d282152..7abab6794 100644 --- a/vuu-ui/packages/vuu-table/src/table/useSelection.ts +++ b/vuu-ui/packages/vuu-table/src/table/useSelection.ts @@ -9,7 +9,7 @@ import { metadataKeys, selectItem, } from "@finos/vuu-utils"; -import { DataSourceRow } from "packages/vuu-data-types"; +import { DataSourceRow } from "@finos/vuu-data-types"; import { useCallback, useRef } from "react"; import { RowClickHandler } from "./dataTableTypes"; diff --git a/vuu-ui/packages/vuu-table/src/table/useTable.ts b/vuu-ui/packages/vuu-table/src/table/useTable.ts index d0a7ed73a..f79664d63 100644 --- a/vuu-ui/packages/vuu-table/src/table/useTable.ts +++ b/vuu-ui/packages/vuu-table/src/table/useTable.ts @@ -108,10 +108,6 @@ export const useTable = ({ size: containerMeasurements.innerSize, }); - console.log( - `rowCount from viewportMeasurements ${viewportMeasurements.rowCount}` - ); - const onSubscribed = useCallback( ({ tableSchema }: DataSourceSubscribedMessage) => { if (tableSchema) { diff --git a/vuu-ui/packages/vuu-theme/css/global.css b/vuu-ui/packages/vuu-theme/css/global.css index 887585287..7c90dfa99 100644 --- a/vuu-ui/packages/vuu-theme/css/global.css +++ b/vuu-ui/packages/vuu-theme/css/global.css @@ -1,15 +1,3 @@ -/** - * Have some global styles to simulate more realistic app. Currently only for Storybook. - */ - - @font-face { - font-family: 'Nunito Sans Regular'; - src: - url('../NunitoSans-Regular.woff') - format('opentype'); - font-weight: normal; - font-style: normal; -} .vuu-theme { color: var(--salt-text-primary-foreground); diff --git a/vuu-ui/packages/vuu-theme/fonts/NunitoSans.css b/vuu-ui/packages/vuu-theme/fonts/NunitoSans.css new file mode 100644 index 000000000..0cd0a402d --- /dev/null +++ b/vuu-ui/packages/vuu-theme/fonts/NunitoSans.css @@ -0,0 +1,58 @@ +/* latin */ +@font-face { + font-family: 'Nunito Sans'; + font-style: normal; + font-weight: 300; + font-stretch: 100%; + font-display: swap; + src: url(./NunitoSansv15.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} /* latin */ +@font-face { + font-family: 'Nunito Sans'; + font-style: normal; + font-weight: 400; + font-stretch: 100%; + font-display: swap; + src: url(./NunitoSansv15.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} /* latin */ +@font-face { + font-family: 'Nunito Sans'; + font-style: normal; + font-weight: 500; + font-stretch: 100%; + font-display: swap; + src: url(./NunitoSansv15.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + /* latin */ +@font-face { + font-family: 'Nunito Sans'; + font-style: normal; + font-weight: 600; + font-stretch: 100%; + font-display: swap; + src: url(./NunitoSansv15.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + /* latin */ + @font-face { + font-family: 'Nunito Sans'; + font-style: normal; + font-weight: 700; + font-stretch: 100%; + font-display: swap; + src: url(./NunitoSansv15.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; + } + /* latin */ + @font-face { + font-family: 'Nunito Sans'; + font-style: normal; + font-weight: 800; + font-stretch: 100%; + font-display: swap; + src: url(./NunitoSansv15.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; + } \ No newline at end of file diff --git a/vuu-ui/packages/vuu-theme/fonts/NunitoSansv15.woff2 b/vuu-ui/packages/vuu-theme/fonts/NunitoSansv15.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..0b28b91a80ef14e8fbb53bf880d3df46ca8cade4 GIT binary patch literal 22144 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 Hdujjxalz~D literal 0 HcmV?d00001 diff --git a/vuu-ui/packages/vuu-theme/index.css b/vuu-ui/packages/vuu-theme/index.css index b31327838..9222247cd 100644 --- a/vuu-ui/packages/vuu-theme/index.css +++ b/vuu-ui/packages/vuu-theme/index.css @@ -1,3 +1,4 @@ +@import url(fonts/NunitoSans.css); @import url(css/global.css); @import url(css/theme.css); @import url(css/components/components.css); diff --git a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/Draggable.tsx b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/Draggable.tsx index 384fc7d36..87391b994 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/Draggable.tsx +++ b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/Draggable.tsx @@ -6,8 +6,9 @@ import { MutableRefObject, TransitionEventHandler, useCallback, + useMemo, } from "react"; -import { PortalDeprecated } from "@finos/vuu-popups"; +import { PopupComponent as Popup, Portal } from "@finos/vuu-popups"; import "./Draggable.css"; @@ -40,15 +41,29 @@ export const Draggable = forwardRef< ); const forkedRef = useForkRef(forwardedRef, callbackRef); + const position = useMemo( + () => ({ + left: 0, + top: 0, + }), + [] + ); + return ( - -
- + + +
+ + ); }); diff --git a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/DropIndicator.tsx b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/DropIndicator.tsx index 89f1d3edd..89fb2095d 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/DropIndicator.tsx +++ b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/DropIndicator.tsx @@ -1,4 +1,4 @@ -import { PortalDeprecated } from "@finos/vuu-popups"; +import { Portal } from "@finos/vuu-popups"; import { forwardRef } from "react"; import { Rect } from "./dragDropTypesNext"; @@ -10,12 +10,12 @@ export const DropIndicator = forwardRef< >(function DropIndicator({ rect }, forwardedRef) { const { left, top, width, height } = rect; return ( - +
- + ); }); diff --git a/vuu-ui/packages/vuu-ui-controls/src/list/common-hooks/useKeyboardNavigation.ts b/vuu-ui/packages/vuu-ui-controls/src/list/common-hooks/useKeyboardNavigation.ts index ca28c8a5e..083c09681 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/list/common-hooks/useKeyboardNavigation.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/list/common-hooks/useKeyboardNavigation.ts @@ -241,6 +241,7 @@ export const useKeyboardNavigation = ({ const setIgnoreFocus = (value: boolean) => (ignoreFocus.current = value); const handleFocus = useCallback(() => { + console.trace(`List useKeyboard focus`); // Ignore focus if mouse has been used if (ignoreFocus.current) { ignoreFocus.current = false; @@ -318,6 +319,7 @@ export const useKeyboardNavigation = ({ const handleKeyDown = useCallback( (e: KeyboardEvent) => { + console.log("handleKeyDown"); if (itemCount > 0 && isNavigationKey(e)) { e.preventDefault(); e.stopPropagation(); diff --git a/vuu-ui/packages/vuu-ui-controls/src/list/useList.ts b/vuu-ui/packages/vuu-ui-controls/src/list/useList.ts index d159a8f55..e4f7742fe 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/list/useList.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/list/useList.ts @@ -71,11 +71,11 @@ export const useList = ({ onKeyboardNavigation?.(evt, nextIndex); }; - console.log( - `useList - defaultSelected ${JSON.stringify(defaultSelected)} - selectedProp ${JSON.stringify(selected)} ` - ); + // console.log( + // `useList + // defaultSelected ${JSON.stringify(defaultSelected)} + // selectedProp ${JSON.stringify(selected)} ` + // ); // TODO where do these belong ? const handleSelect = useCallback( diff --git a/vuu-ui/sample-apps/app-vuu-basket-trader/login.css b/vuu-ui/sample-apps/app-vuu-basket-trader/login.css index 244112209..56812898f 100644 --- a/vuu-ui/sample-apps/app-vuu-basket-trader/login.css +++ b/vuu-ui/sample-apps/app-vuu-basket-trader/login.css @@ -4,7 +4,6 @@ body { flex-direction: column; justify-content: center; margin: 0; - font-family: "Open Sans"; } #root { diff --git a/vuu-ui/sample-apps/app-vuu-basket-trader/public/demo.html b/vuu-ui/sample-apps/app-vuu-basket-trader/public/demo.html index 9dee98ee6..5e53d03ff 100644 --- a/vuu-ui/sample-apps/app-vuu-basket-trader/public/demo.html +++ b/vuu-ui/sample-apps/app-vuu-basket-trader/public/demo.html @@ -9,9 +9,6 @@ - VUU App diff --git a/vuu-ui/sample-apps/app-vuu-basket-trader/public/index.html b/vuu-ui/sample-apps/app-vuu-basket-trader/public/index.html index ecdc5622e..155bead1d 100644 --- a/vuu-ui/sample-apps/app-vuu-basket-trader/public/index.html +++ b/vuu-ui/sample-apps/app-vuu-basket-trader/public/index.html @@ -10,9 +10,6 @@ - VUU App diff --git a/vuu-ui/sample-apps/app-vuu-basket-trader/public/login.html b/vuu-ui/sample-apps/app-vuu-basket-trader/public/login.html index 4ed8d3081..d9fcdf50c 100644 --- a/vuu-ui/sample-apps/app-vuu-basket-trader/public/login.html +++ b/vuu-ui/sample-apps/app-vuu-basket-trader/public/login.html @@ -7,9 +7,6 @@ - - VUU App diff --git a/vuu-ui/sample-apps/app-vuu-basket-trader/public/login.html b/vuu-ui/sample-apps/app-vuu-basket-trader/public/login.html index 4ed8d3081..d9fcdf50c 100644 --- a/vuu-ui/sample-apps/app-vuu-basket-trader/public/login.html +++ b/vuu-ui/sample-apps/app-vuu-basket-trader/public/login.html @@ -7,9 +7,6 @@ - - VUU App diff --git a/vuu-ui/sample-apps/app-vuu-basket-trader/public/login.html b/vuu-ui/sample-apps/app-vuu-basket-trader/public/login.html index 4ed8d3081..d9fcdf50c 100644 --- a/vuu-ui/sample-apps/app-vuu-basket-trader/public/login.html +++ b/vuu-ui/sample-apps/app-vuu-basket-trader/public/login.html @@ -7,9 +7,6 @@ -