From 0badbdc976eb3fb217061e7a80196ecf5ee2a0a6 Mon Sep 17 00:00:00 2001 From: pling-scottlogic <79100986+pling-scottlogic@users.noreply.github.com> Date: Tue, 21 Nov 2023 15:09:57 +0000 Subject: [PATCH] VUU-333: Implement layout server (#970) * restore double quotes * layout-provider * layout-reducer * layout-view * palette * palette * placeholder * layout-view decomment * registry * rollback multi filter dropdown * stack * tabs * config wrapper * tools * utils * layout top level * revert flexbox layout change * restore lost semicolon * missing space * change action to a type union * Update README.md * Sync with Finos main * VUU-41 style fixes * VUU-41 rename css variable to --vuu * Manage layout persistence via interface (#55) * VUU25: Initialise module * VUU25: Basic Controller infrastructure * VUU25: Implement Layout & Metadata models * VUU25: Add OpenAPI docs (Swagger) * VUU25: Add basic H2 database * VUU25: Implement DTOs * VUU-27 interface to return promises * VUU25: Change server port and swagger URL * VUU25: Wire up Controller, Service and Repository for full implementation - Also amend DTOs to differentiate request/response data * VUU25: Implement model mapper to replace manual entity<->DTO conversion * VUU-47 add methods for loading and saving tempLayout * VUU-47 use loadLayoutById in LayoutList * VUU-47 remove unused files * VUU-47 update other examples to use new hook * VUU25: Fix one-to-one entity persistence issue * Calculated column (#882) * calculated column in settings, instrument search * additional mock data sources * instrument tiles * calculated column editing * measured-container * Row used columnMap rathe than column key * full keyboard nav for table * fix drag drop in column group headerr * use MeasuredContainer for Table List * table cell editing updates datasource * table editing * fix type issues * fix old background renderer * remove outdated import in showcase story * exclude PatternValidator from semgrep * add vuu tooltip component (#885) * VUU-47 improve naming * VUU-47 use placeholder in defaultLayout * VUU-47 update docs with new naming * remove duplicate CSS * VUU-47 fix layoutList styling * VUU-47 add loaded layouts to layout view * VUU25: Pluralise layout endpoint resource * VUU25: Amend annotations, javadocs, and updateLayout signature in the Controller * VUU25: Add javadocs to definition field in LayoutDTOs * VUU25: Remove no args constructor on entities * VUU25: Replace layoutRepository with metadataRepository in LayoutService.getMetadata * VUU25: Add date to create layout HTTP response body * VUU25: Introduce MetadataService * VUU-47 rename currentLayout to applicationLayout * VUU25: Create layout controller unit tests (backend) * VUU25: Create layout integration tests (backend) * VUU25: Add global exception handling to give appropriate HTTP responses * VUU25: Rename LayoutResponseDTO to GetLayoutResponseDTO * VUU25: Reformatting * VUU25: Make deleteLayout generate 404 if layout does not exist (backend) * VUU-47 make defaultLayout closeable and update features * VUU25: Downgrade Java 17 -> 11 and Springboot 3 -> 2 * VUU25: Change DB persistence from file to in-memory * VUU25: Fix issues caused by downgrading Java & Springboot * VUU25: Write description for layout-server pom.xml * VUU-27 interface to return promises * VUU-54: Validate IDs in LocalLayoutPersistenceManager * VUU-54: Mock get/saveLocalEntity * VUU-54: Refactor promises * VUU-54: Remove unnecessary asyncs * VUU-54: Use string union to distinguish layouts/metadata * VUU-54: Rename variables * VUU-54: Convert layout types to interfaces * VUU-54: Extract loadAndFilter method * VUU-54: Replace filter with find * VUU-54: Rename validateId variables * VUU-54: Change vars to lets * VUU-54: Update imports for consistency * VUU-54: Add comment to explain filter(Boolean) * VUU-54: Refactor tests * VUU-54: Extract expectError * VUU-54: Remove loadAndFilter method * VUU-54: Remove removeEntry method * VUU-52: Add E2E tests to CI * VUU-52: Use commit hash for cypress-io * VUU-52: Add comment to explain full SHA * VUU25: Implement interface for Metadata DTOs * VUU-47 rename imports * VUU-59 set up notification context * VUU-47 fix cypress test * Update vuu-ui/packages/vuu-layout/src/layout-persistence/LayoutPersistenceManager.ts Co-authored-by: Cara <99646608+cfisher-scottlogic@users.noreply.github.com> * VUU-47 remove unused import * VUU25: Create layout service unit tests (backend) * VUU25: Add test for transactional nature of LayoutService.createLayout() * VUU25: Remove unnecessary LayoutController unit tests * VUU25: Mock out model mapper in LayoutController tests * VUU25: Remove TODO * VUU25: Move 'updated date' logic from DTO to service for update layout requests * VUU-59 notifications with animation * VUU25: Move updateLayout logic from mapper in controller to setters in service * VUU-59 revert changes to imports * VUU25: Fix tests for recent changes in layout server - Update and delete layout in LayoutControllerTest and LayoutServiceTest * VUU25: Remove unnecessary autogenerated content - maven wrapper files - gitignore * VUU25: Make create layout (with a valid layout) integration test more robust * VUU-59 change toast timeout * VUU-59 change notificationType to enum * VUU-27 remotePersistenceManager implementation * VUU-27 https in baseUrl * VUU25: Increase max length of screenshot column * VUU25: Change create layout endpoint to return whole layout object * VUU25: Fix tests for new create layout response * VUU25: Simplify LayoutControllerTest test name - `getLayout_validIdAndLayoutExists_returnsLayout` -> `getLayout_layoutExists_returnsLayout` * VUU-59 improvements to example and add comments * VUU25: Fix tests for changed unidirectional Layout<->Metadata relationship * VUU-27 add cors config * VUU-27 improve error messages * VUU-27 improve error * VUU-27 add type for LayoutMetadataDto * VUU-27 remove redundant instantiation of persistenceManager * VUU-27 improvements to unit tests * VUU25: Implement layout server (#57) Implement the remote server implementation for layout management * VUU-27 make fetch method explicit for "GET" * VUU-70: Create resource for application layouts * VUU-70: Add integration tests * VUU-70: Add unit tests for controller * VUU-70: Fix service test * VUU-70: Extract constant for default layout file * VUU-70: Adjust whitespace * VUU-70: Use AttributeConverter for JsonNode * VUU-70: Remove DTO annotations * VUU-70: Rename method * VUU-70: Refactor getApplicationLayout * VUU-70: Standardise DTO casing * VUU-70: Return 500 on failed JSON read * VUU-70: Adjust exception handling * VUU-27 add check for date * VUU-27 allow BASE_URL env variable * VUU-70: Implement custom error responses * VUU25: Fix tests since merging in main with BaseMetadata * VUU25: Fix exception not being resolved due to invalid parameters * VUU-70: Update javadocs * VUU25: Add response body content message assertion for non-200 test requests * VUU-70: Revert application properties * VUU-27 test improvements * VUU25: Cleanup conflict resolution mistakes from merge commit * #900 disable failing test * 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 * Tidy package and type issues (#914) * fix package references, type issues * add typescript config for correct auto import resolution * add Layout Management Provider to sample apps (#917) * add Layout Management Provider to sample apps * fix test dependencies * Drag drop provider (#918) * add DragDrop example, resume drag in DragProvider * full flow for remote drag * fix type issues * fix post rebase conflicts, type issues * ignore type issue in drag drop code for now, so packages build * Filterbar styling (#919) * improve the keyboards navigation in Toolbar * fix form control styling, uennecessaryb layout rerenders, table resize bug * make sure all table config setting changes are saved, style tweaks * calculated columns * fix dropdown width * fix width of combo in SaveLayout Panel * remove console.log * fix bug in OverflowContainer when orientation vertical * remove global error listener, leave this to cypress * move test schemas out of showcase (#920) * Update dependency electron to v22 [SECURITY] (#894) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * bump vite, vitest versions to latest (#921) * Instrument picker (#923) * make instrument-picker more generic * refactor Table navigation, preparing for row highlighting * fix broken import paths * add empty inlined worker as vitext mocj fails otherwise * reinstate ignore for inlined-wotker so stub file doesn't get overwritten (#924) * VUU-27 refactor to not use appendAndPersist * VUU-70: Add DefaultApplicationLayoutLoader - Introduce DefaultApplicationLayoutLoader to lazily load JSON from a static file - Refactor service to use loader - Add integration test case for failure to load default - Rename static JSON files - Move JsonNodeConverter to utils package * VUU-70: Rename controller test methods * VUU25: Modify exception handling for deleteLayout * Merge branch 'main' of https://github.com/ScottLogic/finos-vuu into VUU-27-remote-layout-management * VUU-27 extract updatedLayout vars * VUU-27 add types for responses * VUU-89 make metadata and layout share ids * VUU-89 fix integration tests * VUU25: Improve non-200 response messages * VUU-70: Rename header key for username - Rename header key from 'user' to 'username' - Add integration tests for missing username * VUU-70: Remove POST endpoint * VUU-89 set metadata.id in Layout id setter and test improvements * Merge finos:main -> sl:main * #900 disable failing test * 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 * Tidy package and type issues (#914) * fix package references, type issues * add typescript config for correct auto import resolution * Layout Management (#916) * fix vuu-filters types * drawer and dialog fix * drag drop flexbox editable * Update vuu-ui/packages/vuu-filters/src/filter-utils.ts Whitespace between guard clauses Co-authored-by: Luke Vincent * layout header * restore double quotes * layout-provider * layout-reducer * layout-view * palette * palette * placeholder * layout-view decomment * registry * rollback multi filter dropdown * stack * tabs * config wrapper * tools * utils * layout top level * revert flexbox layout change * restore lost semicolon * missing space * change action to a type union * Update README.md * Sync with Finos main * VUU-41 style fixes * VUU-41 rename css variable to --vuu * Manage layout persistence via interface (#55) * VUU-27 interface to return promises * VUU-47 add methods for loading and saving tempLayout * VUU-47 use loadLayoutById in LayoutList * VUU-47 remove unused files * VUU-47 update other examples to use new hook * Calculated column (#882) * calculated column in settings, instrument search * additional mock data sources * instrument tiles * calculated column editing * measured-container * Row used columnMap rathe than column key * full keyboard nav for table * fix drag drop in column group headerr * use MeasuredContainer for Table List * table cell editing updates datasource * table editing * fix type issues * fix old background renderer * remove outdated import in showcase story * exclude PatternValidator from semgrep * add vuu tooltip component (#885) * VUU-47 improve naming * VUU-47 use placeholder in defaultLayout * VUU-47 update docs with new naming * remove duplicate CSS * VUU-47 fix layoutList styling * VUU-47 add loaded layouts to layout view * VUU-47 rename currentLayout to applicationLayout * VUU-47 make defaultLayout closeable and update features * VUU-27 interface to return promises * VUU-54: Validate IDs in LocalLayoutPersistenceManager * VUU-54: Mock get/saveLocalEntity * VUU-54: Refactor promises * VUU-54: Remove unnecessary asyncs * VUU-54: Use string union to distinguish layouts/metadata * VUU-54: Rename variables * VUU-54: Convert layout types to interfaces * VUU-54: Extract loadAndFilter method * VUU-54: Replace filter with find * VUU-54: Rename validateId variables * VUU-54: Change vars to lets * VUU-54: Update imports for consistency * VUU-54: Add comment to explain filter(Boolean) * VUU-54: Refactor tests * VUU-54: Extract expectError * VUU-54: Remove loadAndFilter method * VUU-54: Remove removeEntry method * VUU-52: Add E2E tests to CI * VUU-52: Use commit hash for cypress-io * VUU-52: Add comment to explain full SHA * VUU-47 rename imports * VUU-59 set up notification context * VUU-47 fix cypress test * Update vuu-ui/packages/vuu-layout/src/layout-persistence/LayoutPersistenceManager.ts Co-authored-by: Cara <99646608+cfisher-scottlogic@users.noreply.github.com> * VUU-47 remove unused import * VUU-59 notifications with animation * VUU-59 revert changes to imports * VUU-59 change toast timeout * VUU-59 change notificationType to enum * VUU-59 improvements to example and add comments --------- Co-authored-by: harryhartley Co-authored-by: Luke Vincent Co-authored-by: Joe Dunleavy <107405201+Joe-Dunleavy@users.noreply.github.com> Co-authored-by: Joe Dunleavy Co-authored-by: cfisher-scottlogic Co-authored-by: Cara <99646608+cfisher-scottlogic@users.noreply.github.com> Co-authored-by: Peter Ling Co-authored-by: pling-scottlogic <79100986+pling-scottlogic@users.noreply.github.com> Co-authored-by: heswell * add Layout Management Provider to sample apps (#917) * add Layout Management Provider to sample apps * fix test dependencies * Drag drop provider (#918) * add DragDrop example, resume drag in DragProvider * full flow for remote drag * fix type issues * fix post rebase conflicts, type issues * ignore type issue in drag drop code for now, so packages build * Filterbar styling (#919) * improve the keyboards navigation in Toolbar * fix form control styling, uennecessaryb layout rerenders, table resize bug * make sure all table config setting changes are saved, style tweaks * calculated columns * fix dropdown width * fix width of combo in SaveLayout Panel * remove console.log * fix bug in OverflowContainer when orientation vertical * remove global error listener, leave this to cypress * move test schemas out of showcase (#920) * Update dependency electron to v22 [SECURITY] (#894) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * bump vite, vitest versions to latest (#921) * Instrument picker (#923) * make instrument-picker more generic * refactor Table navigation, preparing for row highlighting * fix broken import paths * add empty inlined worker as vitext mocj fails otherwise * reinstate ignore for inlined-wotker so stub file doesn't get overwritten (#924) * disable basket functionality in showcase while we wait for full server (#925) * disable basket functionality in showcase while we wait for server implementation * remove console log * fix styling of main tabs during drag (#929) * wiring together calculated column pieces (#931) * wiring together calculated column pieces * remove logging * re-enable all tests * fix FilterTable resize bug (#932) * move table height fix to measured container (#933) * type fixes (#934) * final styling for calculated column input (#935) * connect filterbar to persistence (#936) * connect filterbar to persistence * skip test for missing TreeWalker finctionality until its there * final adjustments to table column header styling (#937) * move date generators to test data package (#938) * Bump postcss from 8.4.27 to 8.4.31 in /vuu-ui (#926) Bumps [postcss](https://github.com/postcss/postcss) from 8.4.27 to 8.4.31. - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/postcss/postcss/compare/8.4.27...8.4.31) --- updated-dependencies: - dependency-name: postcss dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * #850 added for functionality to the rpc service, not ready for the big time yet, but not far off. * #850 added ability to reference tables in separate modules. * #850 refactored simul module to take prices out to prices module * #850 refactored tableDefContainer to make it an implicit, otherwise it is re-used in the tests causing issues. * #850 found a bug with basket constituents, not resolved as of this commit. * #850 found a bug with basket constituents, not resolved as of this commit. * Update basket tables UI (#940) * update all basket tables, integrate basket server changes * filter basket tables when loading * connect cell editing * remove console log * fix newFeature story * remove console log * fix for dropdown width error * remove console.log * Fix faulty conflict resolutions --------- Signed-off-by: dependabot[bot] Co-authored-by: keikeicheung Co-authored-by: heswell Co-authored-by: Vasco <98337074+vferraro-scottlogic@users.noreply.github.com> Co-authored-by: harryhartley Co-authored-by: Luke Vincent Co-authored-by: Joe Dunleavy <107405201+Joe-Dunleavy@users.noreply.github.com> Co-authored-by: Joe Dunleavy Co-authored-by: Peter Ling Co-authored-by: pling-scottlogic <79100986+pling-scottlogic@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: chris * VUU-89 add unit test for Layout * VUU-89 add id to response and service to return layout * VUU-70: Remove @Validated annotation * VUU-70: Make integration test constants private * VUU-89 sort imports and remove unneeded annotation * Merge finos:main -> sl:main * #900 disable failing test * 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 * Tidy package and type issues (#914) * fix package references, type issues * add typescript config for correct auto import resolution * Layout Management (#916) * fix vuu-filters types * drawer and dialog fix * drag drop flexbox editable * Update vuu-ui/packages/vuu-filters/src/filter-utils.ts Whitespace between guard clauses Co-authored-by: Luke Vincent * layout header * restore double quotes * layout-provider * layout-reducer * layout-view * palette * palette * placeholder * layout-view decomment * registry * rollback multi filter dropdown * stack * tabs * config wrapper * tools * utils * layout top level * revert flexbox layout change * restore lost semicolon * missing space * change action to a type union * Update README.md * Sync with Finos main * VUU-41 style fixes * VUU-41 rename css variable to --vuu * Manage layout persistence via interface (#55) * VUU-27 interface to return promises * VUU-47 add methods for loading and saving tempLayout * VUU-47 use loadLayoutById in LayoutList * VUU-47 remove unused files * VUU-47 update other examples to use new hook * Calculated column (#882) * calculated column in settings, instrument search * additional mock data sources * instrument tiles * calculated column editing * measured-container * Row used columnMap rathe than column key * full keyboard nav for table * fix drag drop in column group headerr * use MeasuredContainer for Table List * table cell editing updates datasource * table editing * fix type issues * fix old background renderer * remove outdated import in showcase story * exclude PatternValidator from semgrep * add vuu tooltip component (#885) * VUU-47 improve naming * VUU-47 use placeholder in defaultLayout * VUU-47 update docs with new naming * remove duplicate CSS * VUU-47 fix layoutList styling * VUU-47 add loaded layouts to layout view * VUU-47 rename currentLayout to applicationLayout * VUU-47 make defaultLayout closeable and update features * VUU-27 interface to return promises * VUU-54: Validate IDs in LocalLayoutPersistenceManager * VUU-54: Mock get/saveLocalEntity * VUU-54: Refactor promises * VUU-54: Remove unnecessary asyncs * VUU-54: Use string union to distinguish layouts/metadata * VUU-54: Rename variables * VUU-54: Convert layout types to interfaces * VUU-54: Extract loadAndFilter method * VUU-54: Replace filter with find * VUU-54: Rename validateId variables * VUU-54: Change vars to lets * VUU-54: Update imports for consistency * VUU-54: Add comment to explain filter(Boolean) * VUU-54: Refactor tests * VUU-54: Extract expectError * VUU-54: Remove loadAndFilter method * VUU-54: Remove removeEntry method * VUU-52: Add E2E tests to CI * VUU-52: Use commit hash for cypress-io * VUU-52: Add comment to explain full SHA * VUU-47 rename imports * VUU-59 set up notification context * VUU-47 fix cypress test * Update vuu-ui/packages/vuu-layout/src/layout-persistence/LayoutPersistenceManager.ts Co-authored-by: Cara <99646608+cfisher-scottlogic@users.noreply.github.com> * VUU-47 remove unused import * VUU-59 notifications with animation * VUU-59 revert changes to imports * VUU-59 change toast timeout * VUU-59 change notificationType to enum * VUU-59 improvements to example and add comments --------- Co-authored-by: harryhartley Co-authored-by: Luke Vincent Co-authored-by: Joe Dunleavy <107405201+Joe-Dunleavy@users.noreply.github.com> Co-authored-by: Joe Dunleavy Co-authored-by: cfisher-scottlogic Co-authored-by: Cara <99646608+cfisher-scottlogic@users.noreply.github.com> Co-authored-by: Peter Ling Co-authored-by: pling-scottlogic <79100986+pling-scottlogic@users.noreply.github.com> Co-authored-by: heswell * add Layout Management Provider to sample apps (#917) * add Layout Management Provider to sample apps * fix test dependencies * Drag drop provider (#918) * add DragDrop example, resume drag in DragProvider * full flow for remote drag * fix type issues * fix post rebase conflicts, type issues * ignore type issue in drag drop code for now, so packages build * Filterbar styling (#919) * improve the keyboards navigation in Toolbar * fix form control styling, uennecessaryb layout rerenders, table resize bug * make sure all table config setting changes are saved, style tweaks * calculated columns * fix dropdown width * fix width of combo in SaveLayout Panel * remove console.log * fix bug in OverflowContainer when orientation vertical * remove global error listener, leave this to cypress * move test schemas out of showcase (#920) * Update dependency electron to v22 [SECURITY] (#894) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * bump vite, vitest versions to latest (#921) * Instrument picker (#923) * make instrument-picker more generic * refactor Table navigation, preparing for row highlighting * fix broken import paths * add empty inlined worker as vitext mocj fails otherwise * reinstate ignore for inlined-wotker so stub file doesn't get overwritten (#924) * disable basket functionality in showcase while we wait for full server (#925) * disable basket functionality in showcase while we wait for server implementation * remove console log * fix styling of main tabs during drag (#929) * wiring together calculated column pieces (#931) * wiring together calculated column pieces * remove logging * re-enable all tests * fix FilterTable resize bug (#932) * move table height fix to measured container (#933) * type fixes (#934) * final styling for calculated column input (#935) * connect filterbar to persistence (#936) * connect filterbar to persistence * skip test for missing TreeWalker finctionality until its there * final adjustments to table column header styling (#937) * move date generators to test data package (#938) * Bump postcss from 8.4.27 to 8.4.31 in /vuu-ui (#926) Bumps [postcss](https://github.com/postcss/postcss) from 8.4.27 to 8.4.31. - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/postcss/postcss/compare/8.4.27...8.4.31) --- updated-dependencies: - dependency-name: postcss dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * #850 added for functionality to the rpc service, not ready for the big time yet, but not far off. * #850 added ability to reference tables in separate modules. * #850 refactored simul module to take prices out to prices module * #850 refactored tableDefContainer to make it an implicit, otherwise it is re-used in the tests causing issues. * #850 found a bug with basket constituents, not resolved as of this commit. * #850 found a bug with basket constituents, not resolved as of this commit. * Update basket tables UI (#940) * update all basket tables, integrate basket server changes * filter basket tables when loading * connect cell editing * remove console log * fix newFeature story * remove console log * fix for dropdown width error * remove console.log * basket workflow (#941) * Fix faulty conflict resolutions --------- Signed-off-by: dependabot[bot] Co-authored-by: keikeicheung Co-authored-by: heswell Co-authored-by: Vasco <98337074+vferraro-scottlogic@users.noreply.github.com> Co-authored-by: harryhartley Co-authored-by: Luke Vincent Co-authored-by: Joe Dunleavy <107405201+Joe-Dunleavy@users.noreply.github.com> Co-authored-by: Joe Dunleavy Co-authored-by: Peter Ling Co-authored-by: pling-scottlogic <79100986+pling-scottlogic@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: chris * VUU86: Add tests for layout server (#68) * merge main in VUU-70 * VUU-70 improve exception handling and clean-up * VUU-70 fix comment * VUU-70 remove helper method * Update layout-server/src/main/java/org/finos/vuu/layoutserver/repository/MetadataRepository.java Co-authored-by: Cara <99646608+cfisher-scottlogic@users.noreply.github.com> * Update layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java Co-authored-by: Cara <99646608+cfisher-scottlogic@users.noreply.github.com> * VUU-89 re-add unit test * VUU-71: Interact with server for application layouts * VUU-71: Extract app layout in remote manager * VUU-71: Rename variable * VUU-71: Apply auto formatting * Sync mains (pull) (#106) * #900 disable failing test * 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 * Tidy package and type issues (#914) * fix package references, type issues * add typescript config for correct auto import resolution * Layout Management (#916) * fix vuu-filters types * drawer and dialog fix * drag drop flexbox editable * Update vuu-ui/packages/vuu-filters/src/filter-utils.ts Whitespace between guard clauses Co-authored-by: Luke Vincent * layout header * restore double quotes * layout-provider * layout-reducer * layout-view * palette * palette * placeholder * layout-view decomment * registry * rollback multi filter dropdown * stack * tabs * config wrapper * tools * utils * layout top level * revert flexbox layout change * restore lost semicolon * missing space * change action to a type union * Update README.md * Sync with Finos main * VUU-41 style fixes * VUU-41 rename css variable to --vuu * Manage layout persistence via interface (#55) * VUU-27 interface to return promises * VUU-47 add methods for loading and saving tempLayout * VUU-47 use loadLayoutById in LayoutList * VUU-47 remove unused files * VUU-47 update other examples to use new hook * Calculated column (#882) * calculated column in settings, instrument search * additional mock data sources * instrument tiles * calculated column editing * measured-container * Row used columnMap rathe than column key * full keyboard nav for table * fix drag drop in column group headerr * use MeasuredContainer for Table List * table cell editing updates datasource * table editing * fix type issues * fix old background renderer * remove outdated import in showcase story * exclude PatternValidator from semgrep * add vuu tooltip component (#885) * VUU-47 improve naming * VUU-47 use placeholder in defaultLayout * VUU-47 update docs with new naming * remove duplicate CSS * VUU-47 fix layoutList styling * VUU-47 add loaded layouts to layout view * VUU-47 rename currentLayout to applicationLayout * VUU-47 make defaultLayout closeable and update features * VUU-27 interface to return promises * VUU-54: Validate IDs in LocalLayoutPersistenceManager * VUU-54: Mock get/saveLocalEntity * VUU-54: Refactor promises * VUU-54: Remove unnecessary asyncs * VUU-54: Use string union to distinguish layouts/metadata * VUU-54: Rename variables * VUU-54: Convert layout types to interfaces * VUU-54: Extract loadAndFilter method * VUU-54: Replace filter with find * VUU-54: Rename validateId variables * VUU-54: Change vars to lets * VUU-54: Update imports for consistency * VUU-54: Add comment to explain filter(Boolean) * VUU-54: Refactor tests * VUU-54: Extract expectError * VUU-54: Remove loadAndFilter method * VUU-54: Remove removeEntry method * VUU-52: Add E2E tests to CI * VUU-52: Use commit hash for cypress-io * VUU-52: Add comment to explain full SHA * VUU-47 rename imports * VUU-59 set up notification context * VUU-47 fix cypress test * Update vuu-ui/packages/vuu-layout/src/layout-persistence/LayoutPersistenceManager.ts Co-authored-by: Cara <99646608+cfisher-scottlogic@users.noreply.github.com> * VUU-47 remove unused import * VUU-59 notifications with animation * VUU-59 revert changes to imports * VUU-59 change toast timeout * VUU-59 change notificationType to enum * VUU-59 improvements to example and add comments --------- Co-authored-by: harryhartley Co-authored-by: Luke Vincent Co-authored-by: Joe Dunleavy <107405201+Joe-Dunleavy@users.noreply.github.com> Co-authored-by: Joe Dunleavy Co-authored-by: cfisher-scottlogic Co-authored-by: Cara <99646608+cfisher-scottlogic@users.noreply.github.com> Co-authored-by: Peter Ling Co-authored-by: pling-scottlogic <79100986+pling-scottlogic@users.noreply.github.com> Co-authored-by: heswell * add Layout Management Provider to sample apps (#917) * add Layout Management Provider to sample apps * fix test dependencies * Drag drop provider (#918) * add DragDrop example, resume drag in DragProvider * full flow for remote drag * fix type issues * fix post rebase conflicts, type issues * ignore type issue in drag drop code for now, so packages build * Filterbar styling (#919) * improve the keyboards navigation in Toolbar * fix form control styling, uennecessaryb layout rerenders, table resize bug * make sure all table config setting changes are saved, style tweaks * calculated columns * fix dropdown width * fix width of combo in SaveLayout Panel * remove console.log * fix bug in OverflowContainer when orientation vertical * remove global error listener, leave this to cypress * move test schemas out of showcase (#920) * Update dependency electron to v22 [SECURITY] (#894) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * bump vite, vitest versions to latest (#921) * Instrument picker (#923) * make instrument-picker more generic * refactor Table navigation, preparing for row highlighting * fix broken import paths * add empty inlined worker as vitext mocj fails otherwise * reinstate ignore for inlined-wotker so stub file doesn't get overwritten (#924) * disable basket functionality in showcase while we wait for full server (#925) * disable basket functionality in showcase while we wait for server implementation * remove console log * fix styling of main tabs during drag (#929) * wiring together calculated column pieces (#931) * wiring together calculated column pieces * remove logging * re-enable all tests * fix FilterTable resize bug (#932) * move table height fix to measured container (#933) * type fixes (#934) * final styling for calculated column input (#935) * connect filterbar to persistence (#936) * connect filterbar to persistence * skip test for missing TreeWalker finctionality until its there * final adjustments to table column header styling (#937) * move date generators to test data package (#938) * Bump postcss from 8.4.27 to 8.4.31 in /vuu-ui (#926) Bumps [postcss](https://github.com/postcss/postcss) from 8.4.27 to 8.4.31. - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/postcss/postcss/compare/8.4.27...8.4.31) --- updated-dependencies: - dependency-name: postcss dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * #850 added for functionality to the rpc service, not ready for the big time yet, but not far off. * #850 added ability to reference tables in separate modules. * #850 refactored simul module to take prices out to prices module * #850 refactored tableDefContainer to make it an implicit, otherwise it is re-used in the tests causing issues. * #850 found a bug with basket constituents, not resolved as of this commit. * #850 found a bug with basket constituents, not resolved as of this commit. * Update basket tables UI (#940) * update all basket tables, integrate basket server changes * filter basket tables when loading * connect cell editing * remove console log * fix newFeature story * remove console log * fix for dropdown width error * remove console.log * basket workflow (#941) * Login panel (#942) * login panel * WIP * tidy up type issues etc * import fixes * remove deprecated example * #944 Added fix for threading related issue in ViewPortContainer.scala when change was called. * #944 Reduced logging * Switch UI to new Theme (#943) * remove sample app * rename basket trading app to sample-app * fix broken inmports * fix bugs persistning table settings * 862 create a basketdesign table that represents the specific instance of a basket that we are modifying it will be based on a basket entry but can be customized to what the user needs (#948) * #862 Fixed test versions to abstract out clock. * #862 Added Viewport scoped rpc service to test. * #862 Added ability to edit baskets join service. * Fix minor UI issues (#949) * fix UI dataTable issue - bugs after resize A column resize operation populates the ColumnState state value. From this point on columnState shadows modelColumns so subsequent changes to modelColumns are not rendered. * enable custom editors for cell renderers Column Settings Panel allows a custom renderer to be selected. Some renderers will have settings of their own. This enables editors form those settings to be made available. * fix type issue * update baskets used in showcase examples to match server tables (#950) * add initial support for columns backed by lookup tables (#951) * move basket data generation into simulated vuu module to pave way for rpc support * add updater for prices, insert for array data source * create Table for test data in modules * fix scrolling issue in table when focusing edit fields * add initial support for table columns backed by lookup tables * wire table cell editing to server calls (#953) * Schema load sequence (#955) * cache table meta requests with promise to avoid multiple server requests * ensure table meta is loaded before client is notified of subcribe table meta for all tables was being requested from server every time any hook asked for the table data. This was happening twice at startup. Now we cache the metadata. If we didn't have table meta at point where CREATE_VP_SUCCESS was handled, we notified client anyway. This caused tables to be rendered with no type information, so alignment was wrong on numerics. * fix all data tests * Merge persistence manager generator approaches * Fix duplicated NavigationStyle export in `TableNext.examples.tsx` --------- Signed-off-by: dependabot[bot] Co-authored-by: keikeicheung Co-authored-by: heswell Co-authored-by: Vasco <98337074+vferraro-scottlogic@users.noreply.github.com> Co-authored-by: harryhartley Co-authored-by: Luke Vincent Co-authored-by: Joe Dunleavy <107405201+Joe-Dunleavy@users.noreply.github.com> Co-authored-by: Joe Dunleavy Co-authored-by: Peter Ling Co-authored-by: pling-scottlogic <79100986+pling-scottlogic@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: chris Co-authored-by: chrisjstevo * VUU80: Refactor Dialog styling to fix multiple components (#100) - Header alignment - Border - Fonts - 'Close' button - Body padding * Sync mains (pull) (#107) * #900 disable failing test * 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 * Tidy package and type issues (#914) * fix package references, type issues * add typescript config for correct auto import resolution * Layout Management (#916) * fix vuu-filters types * drawer and dialog fix * drag drop flexbox editable * Update vuu-ui/packages/vuu-filters/src/filter-utils.ts Whitespace between guard clauses Co-authored-by: Luke Vincent * layout header * restore double quotes * layout-provider * layout-reducer * layout-view * palette * palette * placeholder * layout-view decomment * registry * rollback multi filter dropdown * stack * tabs * config wrapper * tools * utils * layout top level * revert flexbox layout change * restore lost semicolon * missing space * change action to a type union * Update README.md * Sync with Finos main * VUU-41 style fixes * VUU-41 rename css variable to --vuu * Manage layout persistence via interface (#55) * VUU-27 interface to return promises * VUU-47 add methods for loading and saving tempLayout * VUU-47 use loadLayoutById in LayoutList * VUU-47 remove unused files * VUU-47 update other examples to use new hook * Calculated column (#882) * calculated column in settings, instrument search * additional mock data sources * instrument tiles * calculated column editing * measured-container * Row used columnMap rathe than column key * full keyboard nav for table * fix drag drop in column group headerr * use MeasuredContainer for Table List * table cell editing updates datasource * table editing * fix type issues * fix old background renderer * remove outdated import in showcase story * exclude PatternValidator from semgrep * add vuu tooltip component (#885) * VUU-47 improve naming * VUU-47 use placeholder in defaultLayout * VUU-47 update docs with new naming * remove duplicate CSS * VUU-47 fix layoutList styling * VUU-47 add loaded layouts to layout view * VUU-47 rename currentLayout to applicationLayout * VUU-47 make defaultLayout closeable and update features * VUU-27 interface to return promises * VUU-54: Validate IDs in LocalLayoutPersistenceManager * VUU-54: Mock get/saveLocalEntity * VUU-54: Refactor promises * VUU-54: Remove unnecessary asyncs * VUU-54: Use string union to distinguish layouts/metadata * VUU-54: Rename variables * VUU-54: Convert layout types to interfaces * VUU-54: Extract loadAndFilter method * VUU-54: Replace filter with find * VUU-54: Rename validateId variables * VUU-54: Change vars to lets * VUU-54: Update imports for consistency * VUU-54: Add comment to explain filter(Boolean) * VUU-54: Refactor tests * VUU-54: Extract expectError * VUU-54: Remove loadAndFilter method * VUU-54: Remove removeEntry method * VUU-52: Add E2E tests to CI * VUU-52: Use commit hash for cypress-io * VUU-52: Add comment to explain full SHA * VUU-47 rename imports * VUU-59 set up notification context * VUU-47 fix cypress test * Update vuu-ui/packages/vuu-layout/src/layout-persistence/LayoutPersistenceManager.ts Co-authored-by: Cara <99646608+cfisher-scottlogic@users.noreply.github.com> * VUU-47 remove unused import * VUU-59 notifications with animation * VUU-59 revert changes to imports * VUU-59 change toast timeout * VUU-59 change notificationType to enum * VUU-59 improvements to example and add comments --------- Co-authored-by: harryhartley Co-authored-by: Luke Vincent Co-authored-by: Joe Dunleavy <107405201+Joe-Dunleavy@users.noreply.github.com> Co-authored-by: Joe Dunleavy Co-authored-by: cfisher-scottlogic Co-authored-by: Cara <99646608+cfisher-scottlogic@users.noreply.github.com> Co-authored-by: Peter Ling Co-authored-by: pling-scottlogic <79100986+pling-scottlogic@users.noreply.github.com> Co-authored-by: heswell * add Layout Management Provider to sample apps (#917) * add Layout Management Provider to sample apps * fix test dependencies * Drag drop provider (#918) * add DragDrop example, resume drag in DragProvider * full flow for remote drag * fix type issues * fix post rebase conflicts, type issues * ignore type issue in drag drop code for now, so packages build * Filterbar styling (#919) * improve the keyboards navigation in Toolbar * fix form control styling, uennecessaryb layout rerenders, table resize bug * make sure all table config setting changes are saved, style tweaks * calculated columns * fix dropdown width * fix width of combo in SaveLayout Panel * remove console.log * fix bug in OverflowContainer when orientation vertical * remove global error listener, leave this to cypress * move test schemas out of showcase (#920) * Update dependency electron to v22 [SECURITY] (#894) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * bump vite, vitest versions to latest (#921) * Instrument picker (#923) * make instrument-picker more generic * refactor Table navigation, preparing for row highlighting * fix broken import paths * add empty inlined worker as vitext mocj fails otherwise * reinstate ignore for inlined-wotker so stub file doesn't get overwritten (#924) * disable basket functionality in showcase while we wait for full server (#925) * disable basket functionality in showcase while we wait for server implementation * remove console log * fix styling of main tabs during drag (#929) * wiring together calculated column pieces (#931) * wiring together calculated column pieces * remove logging * re-enable all tests * fix FilterTable resize bug (#932) * move table height fix to measured container (#933) * type fixes (#934) * final styling for calculated column input (#935) * connect filterbar to persistence (#936) * connect filterbar to persistence * skip test for missing TreeWalker finctionality until its there * final adjustments to table column header styling (#937) * move date generators to test data package (#938) * Bump postcss from 8.4.27 to 8.4.31 in /vuu-ui (#926) Bumps [postcss](https://github.com/postcss/postcss) from 8.4.27 to 8.4.31. - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/postcss/postcss/compare/8.4.27...8.4.31) --- updated-dependencies: - dependency-name: postcss dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * #850 added for functionality to the rpc service, not ready for the big time yet, but not far off. * #850 added ability to reference tables in separate modules. * #850 refactored simul module to take prices out to prices module * #850 refactored tableDefContainer to make it an implicit, otherwise it is re-used in the tests causing issues. * #850 found a bug with basket constituents, not resolved as of this commit. * #850 found a bug with basket constituents, not resolved as of this commit. * Update basket tables UI (#940) * update all basket tables, integrate basket server changes * filter basket tables when loading * connect cell editing * remove console log * fix newFeature story * remove console log * fix for dropdown width error * remove console.log * basket workflow (#941) * Login panel (#942) * login panel * WIP * tidy up type issues etc * import fixes * remove deprecated example * #944 Added fix for threading related issue in ViewPortContainer.scala when change was called. * #944 Reduced logging * Switch UI to new Theme (#943) * remove sample app * rename basket trading app to sample-app * fix broken inmports * fix bugs persistning table settings * 862 create a basketdesign table that represents the specific instance of a basket that we are modifying it will be based on a basket entry but can be customized to what the user needs (#948) * #862 Fixed test versions to abstract out clock. * #862 Added Viewport scoped rpc service to test. * #862 Added ability to edit baskets join service. * Fix minor UI issues (#949) * fix UI dataTable issue - bugs after resize A column resize operation populates the ColumnState state value. From this point on columnState shadows modelColumns so subsequent changes to modelColumns are not rendered. * enable custom editors for cell renderers Column Settings Panel allows a custom renderer to be selected. Some renderers will have settings of their own. This enables editors form those settings to be made available. * fix type issue * update baskets used in showcase examples to match server tables (#950) * add initial support for columns backed by lookup tables (#951) * move basket data generation into simulated vuu module to pave way for rpc support * add updater for prices, insert for array data source * create Table for test data in modules * fix scrolling issue in table when focusing edit fields * add initial support for table columns backed by lookup tables * wire table cell editing to server calls (#953) * Schema load sequence (#955) * cache table meta requests with promise to avoid multiple server requests * ensure table meta is loaded before client is notified of subcribe table meta for all tables was being requested from server every time any hook asked for the table data. This was happening twice at startup. Now we cache the metadata. If we didn't have table meta at point where CREATE_VP_SUCCESS was handled, we notified client anyway. This caused tables to be rendered with no type information, so alignment was wrong on numerics. * fix all data tests * Issue 850 added the first example module test for rpc services (#954) * #850 Refactored example code so that simulation is in its own module * #850 Refactored example code into its own module * #850 updated documentation * #850 organized imports * #850 added first consumer test outline * #850 added ability to create viewport in test easily * #850 added ability to create viewport in test easily * #850 added first example working test calling viewport Rpc call. * #850 renamed test case. * #850 Added example flow for baskets * #850 Added additional example for editRowAction * #850 Added additional methods for editable example. * #850 Added additional methods for editable example. * #850 Added more functionality to the demo basket app. * #850 Added thorough test of basket functionality. * #850 Fixed test assert which accidentally changed contract in test. * #850 Fixed test assert which accidentally changed contract in test. * #850 Fixed test assert which accidentally changed contract in test. * #850 fixed the bad pom definition. * #850 deleted duplicate and not required config and updated semgrep. * Release 0.9.20 beta (#956) * [maven-release-plugin] prepare release vuu-parent-0.9.20-beta * [maven-release-plugin] prepare for next development iteration --------- Co-authored-by: GitHub Actions * #957 added all projects to release build. (#960) * #957 added all projects to release build. * #957 added all projects to release build. * #957 added all projects to release build. * Release 0.9.33 beta (#961) * [maven-release-plugin] prepare release vuu-parent-0.9.33-beta * [maven-release-plugin] prepare for next development iteration --------- Co-authored-by: GitHub Actions * #957 fixed issue with javadoc. (#963) * Release 0.9.35 beta (#964) * [maven-release-plugin] prepare release vuu-parent-0.9.35-beta * [maven-release-plugin] prepare for next development iteration --------- Co-authored-by: GitHub Actions * enable context menu on basket constituents (#965) * fix behaviour in baskets module in data-test * fix pinned column rendering gap * enable context menu in basket constituent table in basket feature * remove console log * Fix conflict resolutions --------- Signed-off-by: dependabot[bot] Co-authored-by: keikeicheung Co-authored-by: heswell Co-authored-by: Vasco <98337074+vferraro-scottlogic@users.noreply.github.com> Co-authored-by: harryhartley Co-authored-by: Luke Vincent Co-authored-by: Joe Dunleavy <107405201+Joe-Dunleavy@users.noreply.github.com> Co-authored-by: Joe Dunleavy Co-authored-by: Peter Ling Co-authored-by: pling-scottlogic <79100986+pling-scottlogic@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: chris Co-authored-by: chrisjstevo Co-authored-by: GitHub Actions * VUU-333: Post-merge fix * VUU-96: Convert Date to LocalDate * VUU-81 layout definition as ObjectNode * VUU-81 remove unused imports * VUU-81 remove redundant annotation and tidy up * VUU-81 make const naming consistent in unit tests and add comments * VUU-81 remove stringify() for definition in POST * VUU-81 improve naming in test * SLVUU-27: Add allowed origins to CORS config * SLVUU-113: Add `process.env.LOCAL` flag to `esbuild.mjs` (#116) * SLVUU-113: Add `process.env.LOCAL` flag to `esbuild.mjs` * SLVUU-113: Remove runtime null coalescence of envar * SLVUU-113: Add BASE_URL envar to sample-app's esbuild.mjs * SLVUU-113: Add BASE_URL envar to showcase's vite.config.js --------- Signed-off-by: dependabot[bot] Co-authored-by: harryhartley Co-authored-by: Joe Dunleavy <107405201+Joe-Dunleavy@users.noreply.github.com> Co-authored-by: Joe Dunleavy Co-authored-by: cfisher-scottlogic Co-authored-by: Cara <99646608+cfisher-scottlogic@users.noreply.github.com> Co-authored-by: vferraro-scottlogic Co-authored-by: Vasco <98337074+vferraro-scottlogic@users.noreply.github.com> Co-authored-by: heswell Co-authored-by: keikeicheung Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Luke Vincent Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: chris Co-authored-by: chrisjstevo Co-authored-by: GitHub Actions --- .semgrepignore | 1 + layout-server/pom.xml | 78 +++ .../finos/vuu/layoutserver/CorsConfig.java | 20 + .../layoutserver/LayoutServerApplication.java | 13 + .../layoutserver/config/MappingConfig.java | 23 + .../ApplicationLayoutController.java | 62 +++ .../controller/LayoutController.java | 94 ++++ .../dto/request/LayoutRequestDto.java | 19 + .../dto/request/MetadataRequestDto.java | 12 + .../dto/response/ApplicationLayoutDto.java | 10 + .../dto/response/ErrorResponse.java | 24 + .../dto/response/LayoutResponseDto.java | 19 + .../dto/response/MetadataResponseDto.java | 20 + .../exceptions/GlobalExceptionHandler.java | 50 ++ .../InternalServerErrorException.java | 7 + .../layoutserver/model/ApplicationLayout.java | 25 + .../vuu/layoutserver/model/BaseMetadata.java | 19 + .../finos/vuu/layoutserver/model/Layout.java | 30 ++ .../vuu/layoutserver/model/Metadata.java | 32 ++ .../ApplicationLayoutRepository.java | 9 + .../repository/LayoutRepository.java | 10 + .../repository/MetadataRepository.java | 11 + .../service/ApplicationLayoutService.java | 41 ++ .../layoutserver/service/LayoutService.java | 55 ++ .../layoutserver/service/MetadataService.java | 24 + .../utils/DefaultApplicationLayoutLoader.java | 40 ++ .../utils/ObjectNodeConverter.java | 42 ++ .../src/main/resources/application.properties | 9 + .../resources/defaultApplicationLayout.json | 22 + .../LayoutServerApplicationTests.java | 13 + .../ApplicationLayoutControllerTest.java | 63 +++ .../controller/LayoutControllerTest.java | 164 ++++++ .../ApplicationLayoutIntegrationTest.java | 181 +++++++ .../integration/LayoutIntegrationTest.java | 480 ++++++++++++++++++ .../vuu/layoutserver/model/LayoutTest.java | 23 + .../service/ApplicationLayoutServiceTest.java | 97 ++++ .../service/LayoutServiceTest.java | 111 ++++ .../service/MetadataServiceTest.java | 38 ++ .../resources/application-test.properties | 5 + .../resources/defaultApplicationLayout.json | 3 + pom.xml | 1 + .../LayoutPersistenceManager.ts | 8 +- .../LocalLayoutPersistenceManager.ts | 63 +-- .../RemoteLayoutPersistenceManager.ts | 188 +++++++ .../src/layout-persistence/index.ts | 1 + .../useLayoutContextMenuItems.tsx | 5 +- .../LocalLayoutPersistenceManager.test.ts | 134 +++-- .../RemoteLayoutPersistenceManager.test.ts | 304 +++++++++++ .../test/layout-persistence/utils.ts | 8 + .../src/dialog-header/DialogHeader.css | 14 +- .../packages/vuu-popups/src/dialog/Dialog.css | 6 +- .../packages/vuu-popups/src/dialog/Dialog.tsx | 2 +- .../vuu-popups/src/dialog/useDialog.tsx | 2 + .../src/layout-management/LayoutList.tsx | 14 +- .../src/layout-management/SaveLayoutPanel.css | 14 +- .../src/layout-management/SaveLayoutPanel.tsx | 15 +- .../src/layout-management/layoutTypes.ts | 9 +- .../layout-management/useLayoutManager.tsx | 80 +-- .../{ => fonts}/NunitoSans-Regular.woff | Bin .../packages/vuu-theme/fonts/NunitoSans.css | 59 ++- .../vuu-ui-controls/src/inputs/Checkbox.css | 1 + .../src/inputs/RadioButton.css | 1 + .../src/new-basket-panel/NewBasketPanel.css | 9 +- .../src/new-basket-panel/NewBasketPanel.tsx | 6 +- vuu-ui/scripts/esbuild.mjs | 2 + vuu-ui/showcase/vite.config.js | 2 + vuu-ui/tsconfig.json | 18 +- .../vuu/provider/VuuJoinTableProvider.scala | 6 +- 68 files changed, 2786 insertions(+), 185 deletions(-) create mode 100644 layout-server/pom.xml create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/CorsConfig.java create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/LayoutServerApplication.java create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/config/MappingConfig.java 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/controller/LayoutController.java create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDto.java create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDto.java create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/ApplicationLayoutDto.java 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/dto/response/LayoutResponseDto.java create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/MetadataResponseDto.java create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/exceptions/GlobalExceptionHandler.java create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/exceptions/InternalServerErrorException.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/model/BaseMetadata.java create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.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/repository/LayoutRepository.java create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/repository/MetadataRepository.java create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/service/MetadataService.java create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/utils/DefaultApplicationLayoutLoader.java create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/utils/ObjectNodeConverter.java create mode 100644 layout-server/src/main/resources/application.properties create mode 100644 layout-server/src/main/resources/defaultApplicationLayout.json create mode 100644 layout-server/src/test/java/org/finos/vuu/layoutserver/LayoutServerApplicationTests.java create mode 100644 layout-server/src/test/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutControllerTest.java create mode 100644 layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java create mode 100644 layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java create mode 100644 layout-server/src/test/java/org/finos/vuu/layoutserver/integration/LayoutIntegrationTest.java create mode 100644 layout-server/src/test/java/org/finos/vuu/layoutserver/model/LayoutTest.java create mode 100644 layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java create mode 100644 layout-server/src/test/java/org/finos/vuu/layoutserver/service/LayoutServiceTest.java create mode 100644 layout-server/src/test/java/org/finos/vuu/layoutserver/service/MetadataServiceTest.java create mode 100644 layout-server/src/test/resources/application-test.properties create mode 100644 layout-server/src/test/resources/defaultApplicationLayout.json 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 rename vuu-ui/packages/vuu-theme/{ => fonts}/NunitoSans-Regular.woff (100%) diff --git a/.semgrepignore b/.semgrepignore index af2e78047..ded413c74 100644 --- a/.semgrepignore +++ b/.semgrepignore @@ -7,6 +7,7 @@ example/order/src/main/scala/org/finos/vuu/provider/simulation/SimulatedBigInstr 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/pom.xml b/layout-server/pom.xml new file mode 100644 index 000000000..9ae3cf8e5 --- /dev/null +++ b/layout-server/pom.xml @@ -0,0 +1,78 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.7.16 + + + org.finos.vuu + layout-server + 0.0.1-SNAPSHOT + layout-server + A remote server to persist layouts for the Vuu client + + 11 + + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-jpa + + + com.h2database + h2 + runtime + + + org.springdoc + springdoc-openapi-ui + 1.6.12 + + + org.projectlombok + lombok + + + org.modelmapper + modelmapper + 3.1.0 + + + org.jetbrains + annotations + 13.0 + compile + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + 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..5637db23f --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/CorsConfig.java @@ -0,0 +1,20 @@ +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", + "https://127.0.0.1:5173", + "http://127.0.0.1:8443/", + "https://127.0.0.1:8443/" + ) + .allowedMethods("GET", "POST", "PUT", "DELETE"); + } +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/LayoutServerApplication.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/LayoutServerApplication.java new file mode 100644 index 000000000..f0a1d10c7 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/LayoutServerApplication.java @@ -0,0 +1,13 @@ +package org.finos.vuu.layoutserver; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class LayoutServerApplication { + + public static void main(String[] args) { + SpringApplication.run(LayoutServerApplication.class, args); + } + +} 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 new file mode 100644 index 000000000..546d38ca7 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/config/MappingConfig.java @@ -0,0 +1,23 @@ +package org.finos.vuu.layoutserver.config; + +import lombok.RequiredArgsConstructor; +import org.finos.vuu.layoutserver.dto.request.LayoutRequestDto; +import org.finos.vuu.layoutserver.model.Layout; +import org.modelmapper.ModelMapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@RequiredArgsConstructor +@Configuration +public class MappingConfig { + + @Bean + public ModelMapper modelMapper() { + ModelMapper mapper = new ModelMapper(); + + mapper.typeMap(LayoutRequestDto.class, Layout.class) + .addMappings(m -> m.skip(Layout::setId)); + + return mapper; + } +} 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..7db9388d5 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java @@ -0,0 +1,62 @@ +package org.finos.vuu.layoutserver.controller; + +import com.fasterxml.jackson.databind.node.ObjectNode; +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.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +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") +public class ApplicationLayoutController { + + private final ApplicationLayoutService service; + private final ModelMapper mapper; + + /** + * 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 + */ + @ResponseStatus(HttpStatus.OK) + @GetMapping + public ApplicationLayoutDto getApplicationLayout(@RequestHeader("username") String username) { + return mapper.map(service.getApplicationLayout(username), ApplicationLayoutDto.class); + } + + /** + * Creates or updates the unique 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) + @PutMapping + public void persistApplicationLayout(@RequestHeader("username") String username, @RequestBody ObjectNode layoutDefinition) { + service.persistApplicationLayout(username, layoutDefinition); + } + + /** + * 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 + */ + @ResponseStatus(HttpStatus.NO_CONTENT) + @DeleteMapping + public void deleteApplicationLayout(@RequestHeader("username") String username) { + service.deleteApplicationLayout(username); + } +} 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 new file mode 100644 index 000000000..315f04c62 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java @@ -0,0 +1,94 @@ +package org.finos.vuu.layoutserver.controller; + +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.model.Layout; +import org.finos.vuu.layoutserver.service.LayoutService; +import org.finos.vuu.layoutserver.service.MetadataService; +import org.modelmapper.ModelMapper; +import org.springframework.http.HttpStatus; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.List; +import java.util.UUID; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/layouts") +@Validated +public class LayoutController { + + private final LayoutService layoutService; + private final MetadataService metadataService; + private final ModelMapper mapper; + + /** + * Gets the specified layout + * + * @param id ID of the layout to get + * @return the layout + */ + @ResponseStatus(HttpStatus.OK) + @GetMapping("/{id}") + public LayoutResponseDto getLayout(@PathVariable UUID id) { + return mapper.map(layoutService.getLayout(id), LayoutResponseDto.class); + } + + /** + * Gets metadata for all layouts + * + * @return the metadata + */ + @ResponseStatus(HttpStatus.OK) + @GetMapping("/metadata") + public List getMetadata() { + + return metadataService.getMetadata() + .stream() + .map(metadata -> mapper.map(metadata, MetadataResponseDto.class)) + .collect(java.util.stream.Collectors.toList()); + } + + /** + * Creates a new layout + * + * @param layoutToCreate the layout to be created + * @return the layout that has been created, with the autogenerated ID and created date + */ + @ResponseStatus(HttpStatus.CREATED) + @PostMapping + public LayoutResponseDto createLayout(@RequestBody @Valid LayoutRequestDto layoutToCreate) { + Layout layout = mapper.map(layoutToCreate, Layout.class); + + return mapper.map(layoutService.createLayout(layout), LayoutResponseDto.class); + } + + /** + * Updates the specified layout + * + * @param id ID of the layout to update + * @param layout the new layout + */ + @ResponseStatus(HttpStatus.NO_CONTENT) + @PutMapping("/{id}") + public void updateLayout(@PathVariable UUID id, @RequestBody @Valid LayoutRequestDto layout) { + Layout newLayout = mapper.map(layout, Layout.class); + + layoutService.updateLayout(id, newLayout); + } + + /** + * Deletes the specified layout + * + * @param id ID of the layout to delete + */ + @ResponseStatus(HttpStatus.NO_CONTENT) + @DeleteMapping("/{id}") + public void deleteLayout(@PathVariable UUID id) { + layoutService.deleteLayout(id); + } +} 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 new file mode 100644 index 000000000..d1aa93157 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDto.java @@ -0,0 +1,19 @@ +package org.finos.vuu.layoutserver.dto.request; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +@Data +public class LayoutRequestDto { + + /** + * The definition of the layout as an arbitrary JSON structure, describing all required components + */ + @NotNull(message = "Definition must not be null") + private ObjectNode definition; + + @NotNull(message = "Metadata must not be null") + 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 new file mode 100644 index 000000000..abbf99430 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDto.java @@ -0,0 +1,12 @@ +package org.finos.vuu.layoutserver.dto.request; + +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import lombok.Data; +import org.finos.vuu.layoutserver.model.BaseMetadata; + +@Data +public class MetadataRequestDto { + + @JsonUnwrapped + BaseMetadata baseMetadata; +} 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 new file mode 100644 index 000000000..d04d48af5 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/ApplicationLayoutDto.java @@ -0,0 +1,10 @@ +package org.finos.vuu.layoutserver.dto.response; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Data; + +@Data +public class ApplicationLayoutDto { + private String username; + private ObjectNode definition; +} 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..4ba22e316 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/ErrorResponse.java @@ -0,0 +1,24 @@ +package org.finos.vuu.layoutserver.dto.response; + +import lombok.Data; +import org.springframework.http.HttpStatus; + +import javax.servlet.http.HttpServletRequest; +import java.time.LocalDate; +import java.util.List; + +@Data +public class ErrorResponse { + private LocalDate timestamp = LocalDate.now(); + private int status; + private String error; + private List messages; + private String path; + + public ErrorResponse(HttpServletRequest request, List messages, HttpStatus status) { + this.status = status.value(); + this.error = status.getReasonPhrase(); + this.path = request.getRequestURI(); + this.messages = messages; + } +} 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 new file mode 100644 index 000000000..aca2de742 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutResponseDto.java @@ -0,0 +1,19 @@ +package org.finos.vuu.layoutserver.dto.response; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Data; + +import java.util.UUID; + +@Data +public class LayoutResponseDto { + + private UUID id; + + /** + * The definition of the layout as an arbitrary JSON structure, describing all required components + */ + private ObjectNode definition; + + 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 new file mode 100644 index 000000000..236034c1c --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/MetadataResponseDto.java @@ -0,0 +1,20 @@ +package org.finos.vuu.layoutserver.dto.response; + +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import lombok.Data; +import org.finos.vuu.layoutserver.model.BaseMetadata; + +import java.time.LocalDate; +import java.util.UUID; + +@Data +public class MetadataResponseDto { + + private UUID id; + + @JsonUnwrapped + BaseMetadata baseMetadata; + + private LocalDate created; + private LocalDate updated; +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/exceptions/GlobalExceptionHandler.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/exceptions/GlobalExceptionHandler.java new file mode 100644 index 000000000..e0dd4b5c8 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/exceptions/GlobalExceptionHandler.java @@ -0,0 +1,50 @@ +package org.finos.vuu.layoutserver.exceptions; + +import org.finos.vuu.layoutserver.dto.response.ErrorResponse; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; + +import javax.servlet.http.HttpServletRequest; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.stream.Collectors; + +@ControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(NoSuchElementException.class) + public ResponseEntity handleNotFound(HttpServletRequest request, Exception ex) { + HttpStatus status = HttpStatus.NOT_FOUND; + return new ResponseEntity<>(new ErrorResponse(request, List.of(ex.getMessage()), status), status); + } + + @ExceptionHandler({ + HttpMessageNotReadableException.class, + MethodArgumentTypeMismatchException.class}) + public ResponseEntity handleBadRequest(HttpServletRequest request, Exception ex) { + HttpStatus status = HttpStatus.BAD_REQUEST; + return new ResponseEntity<>(new ErrorResponse(request, List.of(ex.getMessage()), status), status); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleMethodArgumentNotValid(HttpServletRequest request, MethodArgumentNotValidException ex) { + HttpStatus status = HttpStatus.BAD_REQUEST; + List errors = ex.getFieldErrors() + .stream() + .map(fieldError -> fieldError.getField() + ": " + fieldError.getDefaultMessage()) + .collect(Collectors.toList()); + return new ResponseEntity<>(new ErrorResponse(request, errors, status), status); + } + + @ExceptionHandler(InternalServerErrorException.class) + public ResponseEntity handleInternalServerError(HttpServletRequest request, Exception ex) { + HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR; + return new ResponseEntity<>(new ErrorResponse(request, List.of(ex.getMessage()), status), status); + + } +} 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/model/ApplicationLayout.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/ApplicationLayout.java new file mode 100644 index 000000000..3ec5631c9 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/ApplicationLayout.java @@ -0,0 +1,25 @@ +package org.finos.vuu.layoutserver.model; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.finos.vuu.layoutserver.utils.ObjectNodeConverter; + +import javax.persistence.Column; +import javax.persistence.Convert; +import javax.persistence.Entity; +import javax.persistence.Id; + +@Data +@Entity +@RequiredArgsConstructor +@AllArgsConstructor +public class ApplicationLayout { + @Id + private String username; + + @Convert(converter = ObjectNodeConverter.class) + @Column(columnDefinition = "JSON") + private ObjectNode definition; +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/BaseMetadata.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/BaseMetadata.java new file mode 100644 index 000000000..2500eb247 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/BaseMetadata.java @@ -0,0 +1,19 @@ +package org.finos.vuu.layoutserver.model; + +import lombok.Data; + +import javax.persistence.Embeddable; +import javax.persistence.Lob; + +@Data +@Embeddable +public class BaseMetadata { + + private String name; + private String group; + + @Lob + private String screenshot; + + private String user; +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java new file mode 100644 index 000000000..7941e1d71 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java @@ -0,0 +1,30 @@ +package org.finos.vuu.layoutserver.model; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Data; +import org.finos.vuu.layoutserver.utils.ObjectNodeConverter; + +import javax.persistence.*; +import java.util.UUID; + +@Data +@Entity +public class Layout { + + @Id + @Column(columnDefinition = "BINARY(16)") + private UUID id; + + @Convert(converter = ObjectNodeConverter.class) + @Column(columnDefinition = "JSON") + private ObjectNode definition; + + @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true) + @JoinColumn(name = "metadata_id", referencedColumnName = "id") + private Metadata metadata; + + public void setId(UUID id) { + this.id = id; + this.metadata.setId(id); + } +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java new file mode 100644 index 000000000..1e5abb00b --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java @@ -0,0 +1,32 @@ +package org.finos.vuu.layoutserver.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Embedded; +import javax.persistence.Entity; +import javax.persistence.Id; +import java.time.LocalDate; +import java.util.UUID; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Entity +public class Metadata { + + @Id + @Column(columnDefinition = "BINARY(16)") + private UUID id; + + @Embedded + private BaseMetadata baseMetadata; + + private final LocalDate created = LocalDate.now(); + + private LocalDate updated; +} 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/repository/LayoutRepository.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/repository/LayoutRepository.java new file mode 100644 index 000000000..19f294ac1 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/repository/LayoutRepository.java @@ -0,0 +1,10 @@ +package org.finos.vuu.layoutserver.repository; + +import org.finos.vuu.layoutserver.model.Layout; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +import java.util.UUID; + +@Repository +public interface LayoutRepository extends CrudRepository {} \ No newline at end of file diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/repository/MetadataRepository.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/repository/MetadataRepository.java new file mode 100644 index 000000000..79ea560d1 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/repository/MetadataRepository.java @@ -0,0 +1,11 @@ +package org.finos.vuu.layoutserver.repository; + +import org.finos.vuu.layoutserver.model.Metadata; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +import java.util.UUID; + + +@Repository +public interface MetadataRepository 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..0727552be --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java @@ -0,0 +1,41 @@ +package org.finos.vuu.layoutserver.service; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.RequiredArgsConstructor; +import org.finos.vuu.layoutserver.model.ApplicationLayout; +import org.finos.vuu.layoutserver.repository.ApplicationLayoutRepository; +import org.finos.vuu.layoutserver.utils.DefaultApplicationLayoutLoader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.stereotype.Service; + +import java.util.NoSuchElementException; + +@RequiredArgsConstructor +@Service +public class ApplicationLayoutService { + + private static final Logger logger = LoggerFactory.getLogger(ApplicationLayoutService.class); + private final ApplicationLayoutRepository repository; + private final DefaultApplicationLayoutLoader defaultLoader; + + public void persistApplicationLayout(String username, ObjectNode layoutDefinition) { + repository.save(new ApplicationLayout(username, layoutDefinition)); + } + + public ApplicationLayout getApplicationLayout(String username) { + return repository.findById(username).orElseGet(() -> { + logger.info("No application layout for user, returning default"); + return defaultLoader.getDefaultLayout(); + }); + } + + public void deleteApplicationLayout(String username) { + try { + repository.deleteById(username); + } catch (EmptyResultDataAccessException e) { + throw new NoSuchElementException("No layout found for user: " + username); + } + } +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java new file mode 100644 index 000000000..2d691728b --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java @@ -0,0 +1,55 @@ +package org.finos.vuu.layoutserver.service; + +import lombok.RequiredArgsConstructor; +import org.finos.vuu.layoutserver.model.Layout; +import org.finos.vuu.layoutserver.model.Metadata; +import org.finos.vuu.layoutserver.repository.LayoutRepository; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.util.NoSuchElementException; +import java.util.UUID; + +@RequiredArgsConstructor +@Service +public class LayoutService { + + private final LayoutRepository layoutRepository; + + public Layout getLayout(UUID id) { + return layoutRepository.findById(id) + .orElseThrow(() -> new NoSuchElementException("Layout with ID '" + id + "' not found")); + } + + public Layout createLayout(Layout layout) { + UUID id = UUID.randomUUID(); + + layout.setId(id); + + return layoutRepository.save(layout); + } + + public void updateLayout(UUID layoutId, Layout newLayout) { + Layout layoutToUpdate = getLayout(layoutId); + Metadata newMetadata = newLayout.getMetadata(); + + Metadata updatedMetadata = Metadata.builder() + .baseMetadata(newMetadata.getBaseMetadata()) + .updated(LocalDate.now()) + .id(layoutToUpdate.getMetadata().getId()) + .build(); + + layoutToUpdate.setDefinition(newLayout.getDefinition()); + layoutToUpdate.setMetadata(updatedMetadata); + + layoutRepository.save(layoutToUpdate); + } + + public void deleteLayout(UUID id) { + try { + layoutRepository.deleteById(id); + } catch (Exception e) { + throw new NoSuchElementException("Layout with ID '" + id + "' not found"); + } + } +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/MetadataService.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/MetadataService.java new file mode 100644 index 000000000..08398edc4 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/MetadataService.java @@ -0,0 +1,24 @@ +package org.finos.vuu.layoutserver.service; + +import lombok.RequiredArgsConstructor; +import org.finos.vuu.layoutserver.model.Metadata; +import org.finos.vuu.layoutserver.repository.MetadataRepository; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +@RequiredArgsConstructor +@Service +public class MetadataService { + + private final MetadataRepository metadataRepository; + + public List getMetadata() { + List metadata = new ArrayList<>(); + + metadataRepository.findAll().forEach(metadata::add); + + return metadata; + } +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/utils/DefaultApplicationLayoutLoader.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/utils/DefaultApplicationLayoutLoader.java new file mode 100644 index 000000000..55abadbe5 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/utils/DefaultApplicationLayoutLoader.java @@ -0,0 +1,40 @@ +package org.finos.vuu.layoutserver.utils; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.finos.vuu.layoutserver.exceptions.InternalServerErrorException; +import org.finos.vuu.layoutserver.model.ApplicationLayout; +import org.springframework.context.annotation.Bean; +import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class DefaultApplicationLayoutLoader { + private static final String DEFAULT_LAYOUT_FILE = "defaultApplicationLayout.json"; + private static ApplicationLayout defaultLayout; + + @Bean + public ApplicationLayout getDefaultLayout() { + if (defaultLayout == null) { + loadDefaultLayout(); + } + return defaultLayout; + } + + private void loadDefaultLayout() { + ObjectNode definition = loadDefaultLayoutJsonFile(); + defaultLayout = new ApplicationLayout(null, definition); + } + + private ObjectNode loadDefaultLayoutJsonFile() { + ObjectMapper objectMapper = new ObjectMapper(); + ClassPathResource resource = new ClassPathResource(DEFAULT_LAYOUT_FILE); + try { + return objectMapper.readValue(resource.getInputStream(), ObjectNode.class); + } catch (IOException e) { + throw new InternalServerErrorException("Failed to read default application layout"); + } + } +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/utils/ObjectNodeConverter.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/utils/ObjectNodeConverter.java new file mode 100644 index 000000000..28eec5186 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/utils/ObjectNodeConverter.java @@ -0,0 +1,42 @@ +package org.finos.vuu.layoutserver.utils; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.persistence.AttributeConverter; +import java.io.IOException; + +public class ObjectNodeConverter implements AttributeConverter { + private static final Logger logger = LoggerFactory.getLogger(ObjectNodeConverter.class); + private static final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public String convertToDatabaseColumn(ObjectNode definition) { + try { + return objectMapper.writeValueAsString(definition); + } catch (final JsonProcessingException e) { + logger.error("JSON writing error", e); + return null; + } + } + + @Override + public ObjectNode convertToEntityAttribute(String definition) { + try { + return objectMapper.readValue(extractDefinition(definition), ObjectNode.class); + } 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/resources/application.properties b/layout-server/src/main/resources/application.properties new file mode 100644 index 000000000..afee88372 --- /dev/null +++ b/layout-server/src/main/resources/application.properties @@ -0,0 +1,9 @@ +server.port=8081 +server.servlet.contextPath=/api +springdoc.swagger-ui.path=/swagger +spring.datasource.url=jdbc:h2:mem:layoutdb;NON_KEYWORDS=GROUP,USER +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password=password +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.h2.console.enabled=true diff --git a/layout-server/src/main/resources/defaultApplicationLayout.json b/layout-server/src/main/resources/defaultApplicationLayout.json new file mode 100644 index 000000000..871b11b44 --- /dev/null +++ b/layout-server/src/main/resources/defaultApplicationLayout.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/LayoutServerApplicationTests.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/LayoutServerApplicationTests.java new file mode 100644 index 000000000..0e56bd365 --- /dev/null +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/LayoutServerApplicationTests.java @@ -0,0 +1,13 @@ +package org.finos.vuu.layoutserver; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class LayoutServerApplicationTests { + + @Test + void contextLoads() { + } + +} 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..bdf1971b8 --- /dev/null +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutControllerTest.java @@ -0,0 +1,63 @@ +package org.finos.vuu.layoutserver.controller; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.finos.vuu.layoutserver.dto.response.ApplicationLayoutDto; +import org.finos.vuu.layoutserver.model.ApplicationLayout; +import org.finos.vuu.layoutserver.service.ApplicationLayoutService; +import org.finos.vuu.layoutserver.utils.ObjectNodeConverter; +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.*; + +class ApplicationLayoutControllerTest { + private static ApplicationLayoutService mockService; + private static ApplicationLayoutController controller; + private static final ModelMapper modelMapper = new ModelMapper(); + private static final ObjectNodeConverter objectNodeConverter = new ObjectNodeConverter(); + + @BeforeEach + public void setup() { + mockService = Mockito.mock(ApplicationLayoutService.class); + controller = new ApplicationLayoutController(mockService, modelMapper); + } + + @Test + public void getApplicationLayout_anyUsername_returnsLayoutFromService() throws JsonProcessingException { + String user = "user"; + ObjectNode definition = objectNodeConverter.convertToEntityAttribute("{\"id\":\"main-tabs\"}"); + + when(mockService.getApplicationLayout(user)) + .thenReturn(new ApplicationLayout(user, definition)); + + ApplicationLayoutDto response = controller.getApplicationLayout(user); + + assertThat(response.getUsername()).isEqualTo(user); + assertThat(response.getDefinition()).isEqualTo(definition); + + verify(mockService, times(1)).getApplicationLayout(user); + } + + @Test + public void persistApplicationLayout_anyInput_callsService() throws JsonProcessingException { + String user = "user"; + ObjectNode definition = objectNodeConverter.convertToEntityAttribute("{\"id\":\"main-tabs\"}"); + + controller.persistApplicationLayout(user, definition); + + verify(mockService, times(1)).persistApplicationLayout(user, definition); + } + + @Test + public void deleteApplicationLayout_anyUsername_callsService() { + String user = "user"; + + controller.deleteApplicationLayout(user); + + verify(mockService, times(1)).deleteApplicationLayout(user); + } +} 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 new file mode 100644 index 000000000..0b128fbad --- /dev/null +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java @@ -0,0 +1,164 @@ +package org.finos.vuu.layoutserver.controller; + +import com.fasterxml.jackson.core.JsonProcessingException; +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.BaseMetadata; +import org.finos.vuu.layoutserver.model.Layout; +import org.finos.vuu.layoutserver.model.Metadata; +import org.finos.vuu.layoutserver.service.LayoutService; +import org.finos.vuu.layoutserver.service.MetadataService; +import org.finos.vuu.layoutserver.utils.ObjectNodeConverter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.modelmapper.ModelMapper; + +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 { + + private static final String LAYOUT_DEFINITION_STRING = "{\"id\":\"main-tabs\"}"; + private static final String LAYOUT_GROUP = "Test Group"; + private static final String LAYOUT_NAME = "Test Layout"; + private static final String LAYOUT_SCREENSHOT = "Test Screenshot"; + private static final String LAYOUT_USER = "Test User"; + private static final UUID VALID_ID = UUID.randomUUID(); + private static final UUID DOES_NOT_EXIST_ID = UUID.randomUUID(); + private static final ObjectNodeConverter objectNodeConverter = new ObjectNodeConverter(); + + @Mock + private LayoutService layoutService; + + @Mock + private MetadataService metadataService; + + @Mock + private ModelMapper modelMapper; + + @InjectMocks + private LayoutController layoutController; + + private Layout layout; + private Metadata metadata; + private BaseMetadata baseMetadata; + private LayoutRequestDto layoutRequest; + private LayoutResponseDto expectedLayoutResponse; + private MetadataResponseDto metadataResponse; + + @BeforeEach + public void setup() throws JsonProcessingException { + baseMetadata = new BaseMetadata(); + baseMetadata.setName(LAYOUT_NAME); + baseMetadata.setUser(LAYOUT_USER); + baseMetadata.setGroup(LAYOUT_GROUP); + baseMetadata.setScreenshot(LAYOUT_SCREENSHOT); + + metadata = Metadata.builder().id(VALID_ID).baseMetadata(baseMetadata).build(); + + layout = new Layout(); + layout.setMetadata(metadata); + layout.setId(VALID_ID); + layout.setDefinition(objectNodeConverter.convertToEntityAttribute(LAYOUT_DEFINITION_STRING)); + + layoutRequest = new LayoutRequestDto(); + MetadataRequestDto metadataRequestDto = new MetadataRequestDto(); + metadataRequestDto.setBaseMetadata(baseMetadata); + layoutRequest.setDefinition(layout.getDefinition()); + layoutRequest.setMetadata(metadataRequestDto); + + metadataResponse = getMetadataResponseDto(); + + expectedLayoutResponse = new LayoutResponseDto(); + expectedLayoutResponse.setId(layout.getId()); + expectedLayoutResponse.setDefinition(layout.getDefinition()); + expectedLayoutResponse.setMetadata(metadataResponse); + + } + + + @Test + void getLayout_layoutExists_returnsLayout() { + when(layoutService.getLayout(VALID_ID)).thenReturn(layout); + when(modelMapper.map(layout, LayoutResponseDto.class)).thenReturn(expectedLayoutResponse); + assertThat(layoutController.getLayout(VALID_ID)).isEqualTo(expectedLayoutResponse); + } + + @Test + void getLayout_layoutDoesNotExist_throwsNoSuchElementException() { + when(layoutService.getLayout(DOES_NOT_EXIST_ID)) + .thenThrow(NoSuchElementException.class); + + assertThrows(NoSuchElementException.class, + () -> layoutController.getLayout(DOES_NOT_EXIST_ID)); + } + + @Test + void getMetadata_metadataExists_returnsMetadata() { + List metadataList = List.of(metadata); + + when(metadataService.getMetadata()).thenReturn(metadataList); + when(modelMapper.map(metadata, MetadataResponseDto.class)) + .thenReturn(metadataResponse); + + assertThat(layoutController.getMetadata()).isEqualTo(List.of(metadataResponse)); + } + + @Test + void getMetadata_noMetadataExists_returnsEmptyArray() { + when(metadataService.getMetadata()).thenReturn(List.of()); + assertThat(layoutController.getMetadata()).isEmpty(); + } + + @Test + void createLayout_validLayout_returnsCreatedLayout() { + Layout layoutWithoutIds = layout; + layoutWithoutIds.setId(null); + + when(modelMapper.map(layoutRequest, Layout.class)).thenReturn(layoutWithoutIds); + when(layoutService.createLayout(layoutWithoutIds)).thenReturn(layout); + when(modelMapper.map(layout, LayoutResponseDto.class)).thenReturn(expectedLayoutResponse); + + assertThat(layoutController.createLayout(layoutRequest)).isEqualTo(expectedLayoutResponse); + } + + @Test + void updateLayout_validLayout_callsLayoutService() { + layout.setId(null); + + when(modelMapper.map(layoutRequest, Layout.class)).thenReturn(layout); + + layoutController.updateLayout(VALID_ID, layoutRequest); + + verify(layoutService).updateLayout(VALID_ID, layout); + } + + @Test + void deleteLayout__validId_callsLayoutService() { + layoutController.deleteLayout(VALID_ID); + + verify(layoutService).deleteLayout(VALID_ID); + } + + private MetadataResponseDto getMetadataResponseDto() { + MetadataResponseDto metadataResponse = new MetadataResponseDto(); + metadataResponse.setId(layout.getId()); + metadataResponse.setBaseMetadata(baseMetadata); + metadataResponse.setCreated(layout.getMetadata().getCreated()); + metadataResponse.setUpdated(layout.getMetadata().getUpdated()); + return metadataResponse; + } +} 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..0f862b852 --- /dev/null +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java @@ -0,0 +1,181 @@ +package org.finos.vuu.layoutserver.integration; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.finos.vuu.layoutserver.exceptions.InternalServerErrorException; +import org.finos.vuu.layoutserver.model.ApplicationLayout; +import org.finos.vuu.layoutserver.repository.ApplicationLayoutRepository; +import org.finos.vuu.layoutserver.utils.DefaultApplicationLayoutLoader; +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.boot.test.mock.mockito.MockBean; +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.*; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +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 { + private static final ObjectMapper objectMapper = new ObjectMapper(); + private static final String BASE_URL = "/application-layouts"; + private static final String MISSING_USERNAME_ERROR_MESSAGE = + "Required request header 'username' for method parameter type String is not present"; + + @Autowired + private MockMvc mockMvc; + @Autowired + private ApplicationLayoutRepository repository; + @MockBean + private DefaultApplicationLayoutLoader mockLoader; + private final DefaultApplicationLayoutLoader realLoader = new DefaultApplicationLayoutLoader(); + + @Test + public void getApplicationLayout_noLayoutExists_returns200WithDefaultLayout() throws Exception { + when(mockLoader.getDefaultLayout()).thenReturn(realLoader.getDefaultLayout()); + + mockMvc.perform(get(BASE_URL).header("username", "new user")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.username", nullValue())) + // Expecting application layout as defined in /test/resources/defaultApplicationLayout.json + .andExpect(jsonPath("$.definition.defaultLayoutKey", is("default-layout-value"))); + } + + @Test + public void getApplicationLayout_defaultFailsToLoad_returns500() throws Exception { + String errorMessage = "Failed to read default application layout"; + doThrow(new InternalServerErrorException(errorMessage)).when(mockLoader).getDefaultLayout(); + + mockMvc.perform(get(BASE_URL).header("username", "new user")) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.messages", iterableWithSize(1))) + .andExpect(jsonPath("$.messages", contains(errorMessage))); + } + + @Test + public void getApplicationLayout_noUserInHeader_returns400() throws Exception { + String actualError = mockMvc.perform(get(BASE_URL)) + .andExpect(status().isBadRequest()) + .andReturn().getResponse().getErrorMessage(); + + assertThat(actualError).isEqualTo(MISSING_USERNAME_ERROR_MESSAGE); + } + + @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(BASE_URL).header("username", user)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.username", is(user))) + .andExpect(jsonPath("$.definition", is(definition))); + } + + @Test + public void persistApplicationLayout_noLayoutExists_returns201AndPersistsLayout() throws Exception { + String user = "user"; + String definition = "{\"key\": \"value\"}"; + + mockMvc.perform(put(BASE_URL).header("username", 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.getDefinition()).isEqualTo(objectMapper.readTree(definition)); + } + + @Test + public void persistApplicationLayout_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(put(BASE_URL).header("username", 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.getDefinition()).isEqualTo(objectMapper.readTree(newDefinition)); + } + + @Test + public void persistApplicationLayout_noUserInHeader_returns400() throws Exception { + String actualError = mockMvc.perform(put(BASE_URL)) + .andExpect(status().isBadRequest()) + .andReturn().getResponse().getErrorMessage(); + + assertThat(actualError).isEqualTo(MISSING_USERNAME_ERROR_MESSAGE); + } + + @Test + public void deleteApplicationLayout_noLayoutExists_returns404() throws Exception { + String user = "user"; + + mockMvc.perform(delete(BASE_URL).header("username", user)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.messages", iterableWithSize(1))) + .andExpect(jsonPath("$.messages", contains("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(BASE_URL).header("username", user)) + .andExpect(status().isNoContent()) + .andExpect(jsonPath("$").doesNotExist()); + + assertThat(repository.findAll()).hasSize(0); + } + + @Test + public void deleteApplicationLayout_noUserInHeader_returns400() throws Exception { + String actualError = mockMvc.perform(delete(BASE_URL)) + .andExpect(status().isBadRequest()) + .andReturn().getResponse().getErrorMessage(); + + assertThat(actualError).isEqualTo(MISSING_USERNAME_ERROR_MESSAGE); + } + + private void persistApplicationLayout(String user, Map definition) { + repository.save(new ApplicationLayout(user, objectMapper.convertValue(definition, ObjectNode.class))); + } +} 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 new file mode 100644 index 000000000..94eedc8e3 --- /dev/null +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/LayoutIntegrationTest.java @@ -0,0 +1,480 @@ +package org.finos.vuu.layoutserver.integration; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.jayway.jsonpath.JsonPath; +import org.finos.vuu.layoutserver.dto.request.LayoutRequestDto; +import org.finos.vuu.layoutserver.dto.request.MetadataRequestDto; +import org.finos.vuu.layoutserver.model.BaseMetadata; +import org.finos.vuu.layoutserver.model.Layout; +import org.finos.vuu.layoutserver.model.Metadata; +import org.finos.vuu.layoutserver.repository.LayoutRepository; +import org.finos.vuu.layoutserver.repository.MetadataRepository; +import org.finos.vuu.layoutserver.utils.ObjectNodeConverter; +import org.junit.jupiter.api.BeforeEach; +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 org.springframework.test.web.servlet.MvcResult; + +import java.util.Map; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.iterableWithSize; +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 LayoutIntegrationTest { + + private static final String DEFAULT_LAYOUT_DEFINITION_STRING = "{\"id\":\"main-tabs\"}"; + private static final String UPDATED_LAYOUT_DEFINITION_STRING = "{\"id\":\"updated-main-tabs\"}"; + private static final String DEFAULT_LAYOUT_NAME = "Default layout name"; + private static final String DEFAULT_LAYOUT_GROUP = "Default layout group"; + private static final String DEFAULT_LAYOUT_SCREENSHOT = "Default layout screenshot"; + private static final String DEFAULT_LAYOUT_USER = "Default layout user"; + private static final UUID DEFAULT_LAYOUT_ID = UUID.fromString("00000000-0000-0000-0000-000000000000"); + + private final ObjectMapper objectMapper = new ObjectMapper(); + private static final ObjectNodeConverter objectNodeConverter = new ObjectNodeConverter(); + + @Autowired + private MockMvc mockMvc; + @Autowired + private LayoutRepository layoutRepository; + @Autowired + private MetadataRepository metadataRepository; + + @BeforeEach + void tearDown() { + layoutRepository.deleteAll(); + metadataRepository.deleteAll(); + } + + @Test + void getLayout_validIDAndLayoutExists_returns200WithLayout() throws Exception { + Layout layout = createDefaultLayoutInDatabase(); + assertThat(layoutRepository.findById(layout.getId()).orElseThrow()).isEqualTo(layout); + + Map definition = objectMapper.convertValue(layout.getDefinition(), Map.class); + + mockMvc.perform(get("/layouts/{id}", layout.getId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.definition", + is(definition))) + .andExpect(jsonPath("$.metadata.name", + is(layout.getMetadata().getBaseMetadata().getName()))) + .andExpect(jsonPath("$.metadata.group", + is(layout.getMetadata().getBaseMetadata().getGroup()))) + .andExpect(jsonPath("$.metadata.screenshot", + is(layout.getMetadata().getBaseMetadata().getScreenshot()))) + .andExpect(jsonPath("$.metadata.user", + is(layout.getMetadata().getBaseMetadata().getUser()))); + } + + @Test + void getLayout_validIDButLayoutDoesNotExist_returns404() throws Exception { + UUID layoutID = UUID.randomUUID(); + + mockMvc.perform(get("/layouts/{id}", layoutID)).andExpect(status().isNotFound()); + } + + @Test + void getLayout_invalidId_returns400() throws Exception { + String layoutID = "invalidUUID"; + + mockMvc.perform(get("/layouts/{id}", layoutID)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.messages", iterableWithSize(1))) + .andExpect(jsonPath("$.messages", + contains("Failed to convert value of type 'java.lang.String' to required type 'java.util" + + ".UUID'; nested exception is java.lang.IllegalArgumentException: Invalid " + + "UUID string: invalidUUID"))); + } + + @Test + void getMetadata_singleMetadataExists_returnsMetadata() throws Exception { + Layout layout = createDefaultLayoutInDatabase(); + assertThat(layoutRepository.findById(layout.getId()).orElseThrow()).isEqualTo(layout); + + mockMvc.perform(get("/layouts/metadata")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].name", + is(layout.getMetadata().getBaseMetadata().getName()))) + .andExpect(jsonPath("$[0].group", + is(layout.getMetadata().getBaseMetadata().getGroup()))) + .andExpect(jsonPath("$[0].screenshot", + is(layout.getMetadata().getBaseMetadata().getScreenshot()))) + .andExpect(jsonPath("$[0].user", + is(layout.getMetadata().getBaseMetadata().getUser()))); + } + + @Test + void getMetadata_multipleMetadataExists_returnsAllMetadata() throws Exception { + UUID layout1Id = UUID.randomUUID(); + UUID layout2Id = UUID.randomUUID(); + Layout layout1 = createLayoutWithIdInDatabase(layout1Id); + Layout layout2 = createLayoutWithIdInDatabase(layout2Id); + layout2.setDefinition(objectNodeConverter.convertToEntityAttribute(UPDATED_LAYOUT_DEFINITION_STRING)); + layout2.getMetadata().getBaseMetadata().setName("Different name"); + layout2.getMetadata().getBaseMetadata().setGroup("Different group"); + layout2.getMetadata().getBaseMetadata().setScreenshot("Different screenshot"); + layout2.getMetadata().getBaseMetadata().setUser("Different user"); + layoutRepository.save(layout2); + + assertThat(layoutRepository.findById(layout1.getId()).orElseThrow()).isEqualTo(layout1); + assertThat(layoutRepository.findById(layout2.getId()).orElseThrow()).isEqualTo(layout2); + + mockMvc.perform(get("/layouts/metadata")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].name", + is(layout1.getMetadata().getBaseMetadata().getName()))) + .andExpect(jsonPath("$[0].group", + is(layout1.getMetadata().getBaseMetadata().getGroup()))) + .andExpect(jsonPath("$[0].screenshot", + is(layout1.getMetadata().getBaseMetadata().getScreenshot()))) + .andExpect(jsonPath("$[0].user", + is(layout1.getMetadata().getBaseMetadata().getUser()))) + .andExpect(jsonPath("$[1].name", + is(layout2.getMetadata().getBaseMetadata().getName()))) + .andExpect(jsonPath("$[1].group", + is(layout2.getMetadata().getBaseMetadata().getGroup()))) + .andExpect(jsonPath("$[1].screenshot", + is(layout2.getMetadata().getBaseMetadata().getScreenshot()))) + .andExpect(jsonPath("$[1].user", + is(layout2.getMetadata().getBaseMetadata().getUser()))); + } + + @Test + void getMetadata_metadataDoesNotExist_returnsEmptyList() throws Exception { + mockMvc.perform(get("/layouts/metadata")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$").isEmpty()); + } + + @Test + void createLayout_validRequest_returnsCreatedLayoutAndLayoutIsPersisted() + throws Exception { + LayoutRequestDto layoutRequest = createValidLayoutRequest(); + + Map definition = objectMapper.convertValue(layoutRequest.getDefinition(), Map.class); + + MvcResult result = mockMvc.perform(post("/layouts") + .content(objectMapper.writeValueAsString(layoutRequest)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id").isNotEmpty()) + .andExpect(jsonPath("$.definition", is(definition))) + .andExpect(jsonPath("$.metadata.name", + is(layoutRequest.getMetadata().getBaseMetadata().getName()))) + .andExpect(jsonPath("$.metadata.group", + is(layoutRequest.getMetadata().getBaseMetadata().getGroup()))) + .andExpect(jsonPath("$.metadata.screenshot", + is(layoutRequest.getMetadata().getBaseMetadata().getScreenshot()))) + .andExpect(jsonPath("$.metadata.user", + is(layoutRequest.getMetadata().getBaseMetadata().getUser()))) + .andReturn(); + + UUID createdLayoutId = UUID.fromString( + JsonPath.read(result.getResponse().getContentAsString(), "$.id")); + Layout createdLayout = layoutRepository.findById(createdLayoutId).orElseThrow(); + Metadata createdMetadata = metadataRepository.findById(createdLayout.getMetadata().getId()) + .orElseThrow(); + + // Check that the one-to-one relationship isn't causing duplicate/unexpected entries in + // the DB + assertThat(layoutRepository.findAll()).containsExactly(createdLayout); + assertThat(metadataRepository.findAll()).containsExactly(createdMetadata); + + assertThat(createdLayout.getDefinition()) + .isEqualTo(layoutRequest.getDefinition()); + assertThat(createdMetadata.getBaseMetadata().getName()) + .isEqualTo(layoutRequest.getMetadata().getBaseMetadata().getName()); + assertThat(createdMetadata.getBaseMetadata().getGroup()) + .isEqualTo(layoutRequest.getMetadata().getBaseMetadata().getGroup()); + assertThat(createdMetadata.getBaseMetadata().getScreenshot()) + .isEqualTo(layoutRequest.getMetadata().getBaseMetadata().getScreenshot()); + assertThat(createdMetadata.getBaseMetadata().getUser()) + .isEqualTo(layoutRequest.getMetadata().getBaseMetadata().getUser()); + } + + @Test + void createLayout_invalidRequestBodyDefinitionsIsNull_returns400AndDoesNotCreateLayout() + throws Exception { + LayoutRequestDto layoutRequest = createValidLayoutRequest(); + layoutRequest.setDefinition(null); + + mockMvc.perform(post("/layouts") + .content(objectMapper.writeValueAsString(layoutRequest)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.messages", iterableWithSize(1))) + .andExpect(jsonPath("$.messages", contains("definition: Definition must not be null"))); + + assertThat(layoutRepository.findAll()).isEmpty(); + assertThat(metadataRepository.findAll()).isEmpty(); + } + + @Test + void createLayout_invalidRequestBodyMetadataIsNull_returns400AndDoesNotCreateLayout() + throws Exception { + LayoutRequestDto layoutRequest = createValidLayoutRequest(); + layoutRequest.setMetadata(null); + + mockMvc.perform(post("/layouts") + .content(objectMapper.writeValueAsString(layoutRequest)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.messages", iterableWithSize(1))) + .andExpect(jsonPath("$.messages", contains("metadata: Metadata must not be null"))); + + assertThat(layoutRepository.findAll()).isEmpty(); + assertThat(metadataRepository.findAll()).isEmpty(); + } + + + @Test + void createLayout_invalidRequestBodyUnexpectedFormat_returns400() throws Exception { + String invalidLayout = "invalidLayout"; + + mockMvc.perform(post("/layouts") + .content(invalidLayout) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.messages", iterableWithSize(1))) + .andExpect(jsonPath("$.messages", contains( + "JSON parse error: Unrecognized token 'invalidLayout': was expecting (JSON " + + "String, Number, Array, Object or token 'null', 'true' or 'false'); nested " + + "exception is com.fasterxml.jackson.core.JsonParseException: Unrecognized " + + "token 'invalidLayout': was expecting (JSON String, Number, Array, Object " + + "or token 'null', 'true' or 'false')\n" + + " at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream);" + + " line: 1, column: 14]"))); + } + + @Test + void updateLayout_validIdAndValidRequest_returns204AndLayoutHasChanged() throws Exception { + Layout initialLayout = createDefaultLayoutInDatabase(); + assertThat(layoutRepository.findById(initialLayout.getId()).orElseThrow()).isEqualTo( + initialLayout); + + LayoutRequestDto layoutRequest = createValidLayoutRequest(); + layoutRequest.setDefinition(objectNodeConverter.convertToEntityAttribute(UPDATED_LAYOUT_DEFINITION_STRING)); + layoutRequest.getMetadata().getBaseMetadata().setName("Updated name"); + layoutRequest.getMetadata().getBaseMetadata().setGroup("Updated group"); + layoutRequest.getMetadata().getBaseMetadata().setScreenshot("Updated screenshot"); + layoutRequest.getMetadata().getBaseMetadata().setUser("Updated user"); + + mockMvc.perform(put("/layouts/{id}", initialLayout.getId()) + .content(objectMapper.writeValueAsString(layoutRequest)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()) + .andExpect(jsonPath("$").doesNotExist()); + + Layout updatedLayout = layoutRepository.findById(initialLayout.getId()).orElseThrow(); + + assertThat(updatedLayout.getDefinition()) + .isEqualTo(layoutRequest.getDefinition()); + assertThat(updatedLayout.getMetadata().getBaseMetadata().getName()) + .isEqualTo(layoutRequest.getMetadata().getBaseMetadata().getName()); + assertThat(updatedLayout.getMetadata().getBaseMetadata().getGroup()) + .isEqualTo(layoutRequest.getMetadata().getBaseMetadata().getGroup()); + assertThat(updatedLayout.getMetadata().getBaseMetadata().getScreenshot()) + .isEqualTo(layoutRequest.getMetadata().getBaseMetadata().getScreenshot()); + assertThat(updatedLayout.getMetadata().getBaseMetadata().getUser()) + .isEqualTo(layoutRequest.getMetadata().getBaseMetadata().getUser()); + + assertThat(updatedLayout).isNotEqualTo(initialLayout); + } + + @Test + void updateLayout_invalidRequestBodyDefinitionIsNull_returns400AndLayoutDoesNotChange() + throws Exception { + Layout layout = createDefaultLayoutInDatabase(); + assertThat(layoutRepository.findById(layout.getId()).orElseThrow()).isEqualTo(layout); + + LayoutRequestDto request = createValidLayoutRequest(); + request.setDefinition(null); + + mockMvc.perform(put("/layouts/{id}", layout.getId()) + .content(objectMapper.writeValueAsString(request)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.messages", iterableWithSize(1))) + .andExpect(jsonPath("$.messages", contains("definition: Definition must not be null"))); + + assertThat(layoutRepository.findById(layout.getId()).orElseThrow()).isEqualTo(layout); + } + + @Test + void updateLayout_invalidRequestBodyMetadataIsNull_returns400AndLayoutDoesNotChange() + throws Exception { + Layout layout = createDefaultLayoutInDatabase(); + assertThat(layoutRepository.findById(layout.getId()).orElseThrow()).isEqualTo(layout); + + LayoutRequestDto request = createValidLayoutRequest(); + request.setMetadata(null); + + mockMvc.perform(put("/layouts/{id}", layout.getId()) + .content(objectMapper.writeValueAsString(request)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.messages", iterableWithSize(1))) + .andExpect(jsonPath("$.messages", contains("metadata: Metadata must not be null"))); + + assertThat(layoutRepository.findById(layout.getId()).orElseThrow()).isEqualTo(layout); + } + + @Test + void updateLayout_invalidRequestBodyUnexpectedFormat_returns400AndLayoutDoesNotChange() + throws Exception { + Layout layout = createDefaultLayoutInDatabase(); + assertThat(layoutRepository.findById(layout.getId()).orElseThrow()).isEqualTo(layout); + + String request = "invalidRequest"; + + mockMvc.perform(put("/layouts/{id}", layout.getId()) + .content(objectMapper.writeValueAsString(request)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.messages", iterableWithSize(1))) + .andExpect(jsonPath("$.messages", contains( + "JSON parse error: Cannot construct instance of `org.finos.vuu.layoutserver.dto" + + ".request.LayoutRequestDto` (although at least one Creator exists): no " + + "String-argument constructor/factory method to deserialize from String " + + "value ('invalidRequest'); nested exception is com.fasterxml.jackson" + + ".databind.exc.MismatchedInputException: Cannot construct instance of `org" + + ".finos.vuu.layoutserver.dto.request.LayoutRequestDto` (although at least " + + "one Creator exists): no String-argument constructor/factory method to " + + "deserialize from String value ('invalidRequest')\n" + + " at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream);" + + " line: 1, column: 1]"))); + + assertThat(layoutRepository.findById(layout.getId()).orElseThrow()).isEqualTo( + layout); + } + + @Test + void updateLayout_validIdButLayoutDoesNotExist_returnsNotFound() throws Exception { + UUID layoutID = UUID.randomUUID(); + LayoutRequestDto layoutRequest = createValidLayoutRequest(); + + mockMvc.perform(put("/layouts/{id}", layoutID) + .content(objectMapper.writeValueAsString(layoutRequest)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + void updateLayout_invalidId_returns400() throws Exception { + String layoutID = "invalidUUID"; + LayoutRequestDto layoutRequest = createValidLayoutRequest(); + + mockMvc.perform(put("/layouts/{id}", layoutID) + .content(objectMapper.writeValueAsString(layoutRequest)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.messages", iterableWithSize(1))) + .andExpect(jsonPath("$.messages", contains( + "Failed to convert value of type 'java.lang.String' to required type 'java.util" + + ".UUID'; nested exception is java.lang.IllegalArgumentException: Invalid " + + "UUID string: invalidUUID"))); + } + + @Test + void deleteLayout_validIdLayoutExists_returnsSuccessAndLayoutIsDeleted() throws Exception { + Layout layout = createDefaultLayoutInDatabase(); + assertThat(layoutRepository.findById(layout.getId()).orElseThrow()).isEqualTo(layout); + + mockMvc.perform(delete("/layouts/{id}", layout.getId())).andExpect(status().isNoContent()); + + assertThat(layoutRepository.findById(layout.getId())).isEmpty(); + } + + @Test + void deleteLayout_validIdLayoutDoesNotExist_returnsNotFound() throws Exception { + UUID layoutID = UUID.randomUUID(); + + mockMvc.perform(delete("/layouts/{id}", layoutID)).andExpect(status().isNotFound()); + } + + @Test + void deleteLayout_invalidId_returns400() throws Exception { + String layoutID = "invalidUUID"; + + mockMvc.perform(delete("/layouts/{id}", layoutID)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.messages", iterableWithSize(1))) + .andExpect(jsonPath("$.messages", contains( + "Failed to convert value of type 'java.lang.String' to required type 'java.util" + + ".UUID'; nested exception is java.lang.IllegalArgumentException: Invalid " + + "UUID string: invalidUUID"))); + } + + private Layout createDefaultLayoutInDatabase() { + Layout layout = new Layout(); + Metadata metadata = new Metadata(); + BaseMetadata baseMetadata = new BaseMetadata(); + + baseMetadata.setName(DEFAULT_LAYOUT_NAME); + baseMetadata.setGroup(DEFAULT_LAYOUT_GROUP); + baseMetadata.setScreenshot(DEFAULT_LAYOUT_SCREENSHOT); + baseMetadata.setUser(DEFAULT_LAYOUT_USER); + + metadata.setBaseMetadata(baseMetadata); + + layout.setDefinition(objectNodeConverter.convertToEntityAttribute(DEFAULT_LAYOUT_DEFINITION_STRING)); + layout.setMetadata(metadata); + layout.setId(DEFAULT_LAYOUT_ID); + + return layoutRepository.save(layout); + } + + private Layout createLayoutWithIdInDatabase(UUID id) { + Layout layout = new Layout(); + Metadata metadata = new Metadata(); + BaseMetadata baseMetadata = new BaseMetadata(); + + baseMetadata.setName(DEFAULT_LAYOUT_NAME); + baseMetadata.setGroup(DEFAULT_LAYOUT_GROUP); + baseMetadata.setScreenshot(DEFAULT_LAYOUT_SCREENSHOT); + baseMetadata.setUser(DEFAULT_LAYOUT_USER); + + metadata.setBaseMetadata(baseMetadata); + + layout.setDefinition(objectNodeConverter.convertToEntityAttribute(DEFAULT_LAYOUT_DEFINITION_STRING)); + layout.setMetadata(metadata); + layout.setId(id); + + return layoutRepository.save(layout); + } + + private LayoutRequestDto createValidLayoutRequest() { + BaseMetadata baseMetadata = new BaseMetadata(); + baseMetadata.setName(DEFAULT_LAYOUT_NAME); + baseMetadata.setGroup(DEFAULT_LAYOUT_GROUP); + baseMetadata.setScreenshot(DEFAULT_LAYOUT_SCREENSHOT); + baseMetadata.setUser(DEFAULT_LAYOUT_USER); + + MetadataRequestDto metadataRequest = new MetadataRequestDto(); + metadataRequest.setBaseMetadata(baseMetadata); + + LayoutRequestDto layoutRequest = new LayoutRequestDto(); + layoutRequest.setDefinition(objectNodeConverter.convertToEntityAttribute(DEFAULT_LAYOUT_DEFINITION_STRING)); + layoutRequest.setMetadata(metadataRequest); + + return layoutRequest; + } +} diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/model/LayoutTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/model/LayoutTest.java new file mode 100644 index 000000000..59d12a436 --- /dev/null +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/model/LayoutTest.java @@ -0,0 +1,23 @@ +package org.finos.vuu.layoutserver.model; + +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +class LayoutTest { + + @Test + void setId_anyId_setsIdForBothLayoutAndMetadata() { + UUID id = UUID.fromString("00000000-0000-0000-0000-000000000000"); + Layout layout = new Layout(); + Metadata metadata = new Metadata(); + layout.setMetadata(metadata); + + layout.setId(id); + + assertThat(layout.getId()).isEqualTo(id); + assertThat(layout.getMetadata().getId()).isEqualTo(id); + } + } 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..e5b8ecb20 --- /dev/null +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java @@ -0,0 +1,97 @@ +package org.finos.vuu.layoutserver.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.finos.vuu.layoutserver.model.ApplicationLayout; +import org.finos.vuu.layoutserver.repository.ApplicationLayoutRepository; +import org.finos.vuu.layoutserver.utils.DefaultApplicationLayoutLoader; +import org.finos.vuu.layoutserver.utils.ObjectNodeConverter; +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 DefaultApplicationLayoutLoader defaultLoader = new DefaultApplicationLayoutLoader(); + private static final ObjectNodeConverter objectNodeConverter = new ObjectNodeConverter(); + + @BeforeEach + public void setup() { + mockRepo = Mockito.mock(ApplicationLayoutRepository.class); + service = new ApplicationLayoutService(mockRepo, defaultLoader); + } + + @Test + public void getApplicationLayout_noLayout_returnsDefault() throws JsonProcessingException { + when(mockRepo.findById(anyString())).thenReturn(Optional.empty()); + + ApplicationLayout actualLayout = service.getApplicationLayout("new user"); + + // Expecting application layout as defined in /test/resources/defaultApplicationLayout.json + ObjectNode expectedDefinition = objectNodeConverter.convertToEntityAttribute("{\"defaultLayoutKey\":\"default-layout-value\"}"); + + assertThat(actualLayout.getUsername()).isNull(); + assertThat(actualLayout.getDefinition()).isEqualTo(expectedDefinition); + } + + @Test + public void getApplicationLayout_layoutExists_returnsLayout() throws JsonProcessingException { + String user = "user"; + + ObjectNode expectedDefinition = objectNodeConverter.convertToEntityAttribute("{\"id\":\"main-tabs\"}"); + ApplicationLayout expectedLayout = new ApplicationLayout(user, expectedDefinition); + + when(mockRepo.findById(user)).thenReturn(Optional.of(expectedLayout)); + + ApplicationLayout actualLayout = service.getApplicationLayout(user); + + assertThat(actualLayout).isEqualTo(expectedLayout); + } + + @Test + public void createApplicationLayout_validDefinition_callsRepoSave() throws JsonProcessingException { + String user = "user"; + ObjectNode definition = objectNodeConverter.convertToEntityAttribute("{\"id\":\"main-tabs\"}"); + + service.persistApplicationLayout(user, definition); + + verify(mockRepo, times(1)) + .save(new ApplicationLayout(user, 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); + } +} diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/service/LayoutServiceTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/service/LayoutServiceTest.java new file mode 100644 index 000000000..42af579a0 --- /dev/null +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/service/LayoutServiceTest.java @@ -0,0 +1,111 @@ +package org.finos.vuu.layoutserver.service; + +import org.finos.vuu.layoutserver.model.BaseMetadata; +import org.finos.vuu.layoutserver.model.Layout; +import org.finos.vuu.layoutserver.model.Metadata; +import org.finos.vuu.layoutserver.repository.LayoutRepository; +import org.finos.vuu.layoutserver.utils.ObjectNodeConverter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.dao.EmptyResultDataAccessException; + +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class LayoutServiceTest { + + private static final UUID LAYOUT_ID = UUID.randomUUID(); + + @Mock + private LayoutRepository layoutRepository; + + @InjectMocks + private LayoutService layoutService; + + private Layout layout; + + private static final ObjectNodeConverter objectNodeConverter = new ObjectNodeConverter(); + + @BeforeEach + public void setup() { + BaseMetadata baseMetadata = new BaseMetadata(); + baseMetadata.setName("Test Name"); + baseMetadata.setGroup("Test Group"); + baseMetadata.setScreenshot("Test Screenshot"); + baseMetadata.setUser("Test User"); + + Metadata metadata = Metadata.builder().id(LAYOUT_ID).baseMetadata(baseMetadata).build(); + + layout = new Layout(); + layout.setMetadata(metadata); + layout.setId(LAYOUT_ID); + layout.setDefinition(objectNodeConverter.convertToEntityAttribute("{\"id\":\"main-tabs\"}")); + } + + @Test + void getLayout_layoutExists_returnsLayout() { + when(layoutRepository.findById(LAYOUT_ID)).thenReturn(Optional.of(layout)); + + assertThat(layoutService.getLayout(LAYOUT_ID)).isEqualTo(layout); + } + + @Test + void getLayout_noLayoutsExist_throwsNotFoundException() { + when(layoutRepository.findById(LAYOUT_ID)).thenReturn(Optional.empty()); + + assertThrows(NoSuchElementException.class, + () -> layoutService.getLayout(LAYOUT_ID)); + } + + @Test + void createLayout_anyLayout_returnsNewLayout() { + when(layoutRepository.save(layout)).thenReturn(layout); + + assertThat(layoutService.createLayout(layout)).isEqualTo(layout); + } + + @Test + void updateLayout_layoutExists_callsRepositorySave() { + when(layoutRepository.findById(LAYOUT_ID)).thenReturn(Optional.of(layout)); + + layoutService.updateLayout(LAYOUT_ID, layout); + + verify(layoutRepository, times(1)).save(layout); + } + + @Test + void updateLayout_layoutDoesNotExist_throwsNoSuchElementException() { + when(layoutRepository.findById(LAYOUT_ID)).thenReturn(Optional.empty()); + + assertThrows(NoSuchElementException.class, + () -> layoutService.updateLayout(LAYOUT_ID, layout)); + } + + @Test + void deleteLayout_anyUUID_callsRepositoryDeleteById() { + layoutService.deleteLayout(LAYOUT_ID); + + verify(layoutRepository, times(1)).deleteById(LAYOUT_ID); + } + + @Test + void deleteLayout_noLayoutExists_throwsNoSuchElementException() { + doThrow(new EmptyResultDataAccessException(1)) + .when(layoutRepository).deleteById(LAYOUT_ID); + + assertThrows(NoSuchElementException.class, + () -> layoutService.deleteLayout(LAYOUT_ID)); + + verify(layoutRepository, times(1)).deleteById(LAYOUT_ID); + } +} \ No newline at end of file diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/service/MetadataServiceTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/service/MetadataServiceTest.java new file mode 100644 index 000000000..74bbb4844 --- /dev/null +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/service/MetadataServiceTest.java @@ -0,0 +1,38 @@ +package org.finos.vuu.layoutserver.service; + +import org.finos.vuu.layoutserver.model.Metadata; +import org.finos.vuu.layoutserver.repository.MetadataRepository; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class MetadataServiceTest { + + @Mock + private MetadataRepository metadataRepository; + + @InjectMocks + private MetadataService metadataService; + + @Test + void getMetadata_metadataExists_returnsMetadata() { + Metadata metadata = Metadata.builder().build(); + + when(metadataRepository.findAll()).thenReturn(List.of(metadata)); + assertThat(metadataService.getMetadata()).isEqualTo(List.of(metadata)); + } + + @Test + void getMetadata_noMetadataExists_returnsEmptyList() { + when(metadataRepository.findAll()).thenReturn(List.of()); + assertThat(metadataService.getMetadata()).isEqualTo(List.of()); + } +} \ No newline at end of file diff --git a/layout-server/src/test/resources/application-test.properties b/layout-server/src/test/resources/application-test.properties new file mode 100644 index 000000000..2722b4aca --- /dev/null +++ b/layout-server/src/test/resources/application-test.properties @@ -0,0 +1,5 @@ +spring.datasource.url=jdbc:h2:mem:testdb;NON_KEYWORDS=GROUP,USER +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password=password +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect \ No newline at end of file diff --git a/layout-server/src/test/resources/defaultApplicationLayout.json b/layout-server/src/test/resources/defaultApplicationLayout.json new file mode 100644 index 000000000..87a79e544 --- /dev/null +++ b/layout-server/src/test/resources/defaultApplicationLayout.json @@ -0,0 +1,3 @@ +{ + "defaultLayoutKey": "default-layout-value" +} diff --git a/pom.xml b/pom.xml index 96462f744..0625fbcf6 100644 --- a/pom.xml +++ b/pom.xml @@ -82,6 +82,7 @@ toolbox vuu vuu-ui + layout-server benchmark example 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 88371f969..88ca5bf96 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 { /** @@ -11,9 +11,9 @@ export interface LayoutPersistenceManager { * @returns Unique identifier assigned to the saved layout */ createLayout: ( - metadata: Omit, + metadata: LayoutMetadataDto, layout: LayoutJSON - ) => Promise; + ) => Promise; /** * Overwrites an existing layout and its corresponding metadata with the provided information @@ -24,7 +24,7 @@ export interface LayoutPersistenceManager { */ updateLayout: ( id: string, - metadata: Omit, + metadata: LayoutMetadataDto, layout: LayoutJSON ) => Promise; 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 c1754ed1d..519dde763 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-persistence/LocalLayoutPersistenceManager.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-persistence/LocalLayoutPersistenceManager.ts @@ -1,7 +1,12 @@ -import { Layout, LayoutMetadata, WithId } from "@finos/vuu-shell"; +import { + Layout, + LayoutMetadata, + LayoutMetadataDto, + 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"; @@ -16,21 +21,24 @@ export class LocalLayoutPersistenceManager implements LayoutPersistenceManager { } } createLayout( - metadata: Omit, + metadata: LayoutMetadataDto, layout: LayoutJSON - ): Promise { + ): Promise { return new Promise((resolve) => { Promise.all([this.loadLayouts(), this.loadMetadata()]).then( ([existingLayouts, existingMetadata]) => { const id = getUniqueId(); - this.appendAndPersist( + const newMetadata: LayoutMetadata = { + ...metadata, id, - metadata, - layout, - existingLayouts, - existingMetadata + created: formatDate(new Date(), "dd.mm.yyyy"), + }; + + this.saveLayoutsWithMetadata( + [...existingLayouts, { id, json: layout }], + [...existingMetadata, newMetadata] ); - resolve(id); + resolve(newMetadata); } ); }); @@ -38,18 +46,20 @@ export class LocalLayoutPersistenceManager implements LayoutPersistenceManager { updateLayout( id: string, - newMetadata: Omit, + newMetadata: LayoutMetadataDto, newLayout: LayoutJSON ): Promise { return new Promise((resolve, reject) => { 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 updatedLayouts = existingLayouts.map((layout) => + layout.id === id ? { ...layout, json: newLayout } : layout + ); + const updatedMetadata = existingMetadata.map((metadata) => + metadata.id === id ? { ...metadata, ...newMetadata } : metadata ); - this.appendAndPersist(id, newMetadata, newLayout, layouts, metadata); + this.saveLayoutsWithMetadata(updatedLayouts, updatedMetadata); resolve(); }) .catch((e) => reject(e)); @@ -77,10 +87,14 @@ export class LocalLayoutPersistenceManager implements LayoutPersistenceManager { this.validateId(id, "layout") .then(() => this.loadLayouts()) .then((existingLayouts) => { - const layouts = existingLayouts.find( + const foundLayout = existingLayouts.find( (layout) => layout.id === id - ) as Layout; - resolve(layouts.json); + ); + if (foundLayout) { + resolve(foundLayout.json); + } else { + reject(new Error(`no layout found matching id ${id}`)); + } }) .catch((e) => reject(e)); }); @@ -122,19 +136,6 @@ export class LocalLayoutPersistenceManager implements LayoutPersistenceManager { }); } - private appendAndPersist( - newId: string, - newMetadata: Omit, - newLayout: LayoutJSON, - existingLayouts: Layout[], - existingMetadata: LayoutMetadata[] - ) { - existingLayouts.push({ id: newId, json: newLayout }); - existingMetadata.push({ id: newId, ...newMetadata }); - - this.saveLayoutsWithMetadata(existingLayouts, existingMetadata); - } - private saveLayoutsWithMetadata( layouts: Layout[], metadata: LayoutMetadata[] 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..14f2af95a --- /dev/null +++ b/vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts @@ -0,0 +1,188 @@ +import { + ApplicationLayout, + LayoutMetadata, + LayoutMetadataDto, +} from "@finos/vuu-shell"; +import { LayoutPersistenceManager } from "./LayoutPersistenceManager"; +import { LayoutJSON } from "../layout-reducer"; + +const baseURL = process.env.LAYOUT_BASE_URL; +const metadataSaveLocation = "layouts/metadata"; +const layoutsSaveLocation = "layouts"; +const applicationLayoutsSaveLocation = "application-layouts"; + +export type CreateLayoutResponseDto = { metadata: LayoutMetadata }; +export type GetLayoutResponseDto = { definition: LayoutJSON }; + +export class RemoteLayoutPersistenceManager + implements LayoutPersistenceManager +{ + createLayout( + metadata: LayoutMetadataDto, + layout: LayoutJSON + ): Promise { + return new Promise((resolve, reject) => + fetch(`${baseURL}/${layoutsSaveLocation}`, { + headers: { + "Content-Type": "application/json", + }, + method: "POST", + body: JSON.stringify({ + metadata, + definition: layout, + }), + }) + .then((response) => { + if (!response.ok) { + reject(new Error(response.statusText)); + } + response.json().then(({ metadata }: CreateLayoutResponseDto) => { + if (!metadata) { + reject(new Error("Response did not contain valid metadata")); + } + resolve(metadata); + }); + }) + .catch((error: Error) => { + reject(error); + }) + ); + } + + updateLayout( + id: string, + metadata: LayoutMetadataDto, + 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}`, { + method: "GET", + }) + .then((response) => { + if (!response.ok) { + reject(new Error(response.statusText)); + } + response.json().then(({ definition }: GetLayoutResponseDto) => { + if (!definition) { + reject(new Error("Response did not contain a valid layout")); + } + resolve(definition); + }); + }) + .catch((error: Error) => { + reject(error); + }); + }); + } + + loadMetadata(): Promise { + return new Promise((resolve, reject) => + fetch(`${baseURL}/${metadataSaveLocation}`, { + method: "GET", + }) + .then((response) => { + if (!response.ok) { + reject(new Error(response.statusText)); + } + response.json().then((metadata: LayoutMetadata[]) => { + if (!metadata) { + reject(new Error("Response did not contain valid metadata")); + } + resolve(metadata); + }); + }) + .catch((error: Error) => { + reject(error); + }) + ); + } + + saveApplicationLayout(layout: LayoutJSON): Promise { + return new Promise((resolve, reject) => + fetch(`${baseURL}/${applicationLayoutsSaveLocation}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + username: "vuu-user", + }, + body: JSON.stringify(layout), + }) + .then((response) => { + if (!response.ok) { + reject(new Error(response.statusText)); + } + resolve(); + }) + .catch((error: Error) => { + reject(error); + }) + ); + } + + loadApplicationLayout(): Promise { + return new Promise((resolve, reject) => + fetch(`${baseURL}/${applicationLayoutsSaveLocation}`, { + method: "GET", + headers: { + username: "vuu-user", + }, + }) + .then((response) => { + if (!response.ok) { + reject(new Error(response.statusText)); + } + response.json().then((applicationLayout: ApplicationLayout) => { + if (!applicationLayout) { + reject( + new Error( + "Response did not contain valid application layout information" + ) + ); + } + resolve(applicationLayout.definition); + }); + }) + .catch((error: Error) => { + reject(error); + }) + ); + } +} 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 10bbeed00..c00fe72d9 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-persistence/index.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-persistence/index.ts @@ -1,4 +1,5 @@ export * from "./data"; export * from "./LayoutPersistenceManager"; export * from "./LocalLayoutPersistenceManager"; +export * from './RemoteLayoutPersistenceManager'; export * from "./useLayoutContextMenuItems"; diff --git a/vuu-ui/packages/vuu-layout/src/layout-persistence/useLayoutContextMenuItems.tsx b/vuu-ui/packages/vuu-layout/src/layout-persistence/useLayoutContextMenuItems.tsx index 547f63f77..d0e746636 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-persistence/useLayoutContextMenuItems.tsx +++ b/vuu-ui/packages/vuu-layout/src/layout-persistence/useLayoutContextMenuItems.tsx @@ -1,5 +1,5 @@ import { - LayoutMetadata, + LayoutMetadataDto, SaveLayoutPanel, useLayoutManager, } from "@finos/vuu-shell"; @@ -19,7 +19,7 @@ export const useLayoutContextMenuItems = (setDialogState: SetDialog) => { }, [setDialogState]); const handleSave = useCallback( - (layoutMetadata: Omit) => { + (layoutMetadata: LayoutMetadataDto) => { saveLayout(layoutMetadata); setDialogState(undefined); }, @@ -63,6 +63,7 @@ export const useLayoutContextMenuItems = (setDialogState: SetDialog) => { /> ), title: "Save Layout", + hideCloseButton: true, }); return true; } 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 a94ccc200..200dbbda3 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,5 +1,5 @@ import "../global-mocks"; -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"; @@ -7,6 +7,8 @@ import { getLocalEntity, saveLocalEntity, } from "../../../vuu-filters/src/local-config"; +import { formatDate } from "@finos/vuu-utils"; +import { expectPromiseRejectsWithError } from "./utils"; vi.mock("@finos/vuu-filters", async () => { return { @@ -29,13 +31,15 @@ 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 = { @@ -43,12 +47,19 @@ const existingLayout: Layout = { json: { type: "t0" }, }; -const metadataToAdd: Omit = { +const metadataToAdd: LayoutMetadataDto = { + name: "New Layout", + group: "Group 1", + screenshot: "screenshot", + user: "vuu user", +}; + +const metadataToUpdate: Omit = { name: "New Layout", group: "Group 1", screenshot: "screenshot", user: "vuu user", - date: "26/09/2023", + created: newDate, }; const layoutToAdd: LayoutJSON = { @@ -63,8 +74,8 @@ afterEach(() => { }); describe("createLayout", () => { - it("persists to local storage with a unique ID", async () => { - const returnedId = await persistenceManager.createLayout( + it("persists to local storage with a unique ID and current date", async () => { + const { id, created } = await persistenceManager.createLayout( metadataToAdd, layoutToAdd ); @@ -75,14 +86,16 @@ describe("createLayout", () => { const expectedMetadata: LayoutMetadata = { ...metadataToAdd, - id: returnedId, + id, + created, }; const expectedLayout: Layout = { json: layoutToAdd, - id: returnedId, + id, }; + expect(created).toEqual(newDate); expect(persistedMetadata).toEqual([expectedMetadata]); expect(persistedLayout).toEqual([expectedLayout]); }); @@ -91,11 +104,11 @@ describe("createLayout", () => { saveLocalEntity(metadataSaveLocation, [existingMetadata]); saveLocalEntity(layoutsSaveLocation, [existingLayout]); - const returnedId = await persistenceManager.createLayout( + const { id, created } = await persistenceManager.createLayout( metadataToAdd, layoutToAdd ); - expect(returnedId).not.toEqual(existingId); + expect(id).not.toEqual(existingId); const persistedMetadata = getLocalEntity(metadataSaveLocation); @@ -103,12 +116,13 @@ describe("createLayout", () => { const expectedMetadata: LayoutMetadata = { ...metadataToAdd, - id: returnedId, + id, + created, }; const expectedLayout: Layout = { json: layoutToAdd, - id: returnedId, + id, }; expect(persistedMetadata).toEqual([existingMetadata, expectedMetadata]); @@ -123,7 +137,7 @@ describe("updateLayout", () => { await persistenceManager.updateLayout( existingId, - metadataToAdd, + metadataToUpdate, layoutToAdd ); @@ -132,7 +146,7 @@ describe("updateLayout", () => { const persistedLayout = getLocalEntity(layoutsSaveLocation); const expectedMetadata: LayoutMetadata = { - ...metadataToAdd, + ...metadataToUpdate, id: existingId, }; @@ -148,9 +162,13 @@ describe("updateLayout", () => { it("errors if there is no metadata in local storage with requested ID ", async () => { saveLocalEntity(layoutsSaveLocation, [existingLayout]); - expectError( + expectPromiseRejectsWithError( () => - persistenceManager.updateLayout(existingId, metadataToAdd, layoutToAdd), + persistenceManager.updateLayout( + existingId, + metadataToUpdate, + layoutToAdd + ), `No metadata with ID ${existingId}` ); }); @@ -158,9 +176,13 @@ describe("updateLayout", () => { it("errors if there is no layout in local storage with requested ID ", async () => { saveLocalEntity(metadataSaveLocation, [existingMetadata]); - expectError( + expectPromiseRejectsWithError( () => - persistenceManager.updateLayout(existingId, metadataToAdd, layoutToAdd), + persistenceManager.updateLayout( + existingId, + metadataToUpdate, + layoutToAdd + ), `No layout with ID ${existingId}` ); }); @@ -168,11 +190,11 @@ describe("updateLayout", () => { it("errors if there is no metadata or layout in local storage with requested ID ", async () => { const requestedId = "non_existent_id"; - expectError( + expectPromiseRejectsWithError( () => persistenceManager.updateLayout( requestedId, - metadataToAdd, + metadataToUpdate, layoutToAdd ), `No metadata with ID ${requestedId}; No layout with ID ${requestedId}` @@ -183,9 +205,13 @@ describe("updateLayout", () => { saveLocalEntity(metadataSaveLocation, [existingMetadata, existingMetadata]); saveLocalEntity(layoutsSaveLocation, [existingLayout]); - expectError( + expectPromiseRejectsWithError( () => - persistenceManager.updateLayout(existingId, metadataToAdd, layoutToAdd), + persistenceManager.updateLayout( + existingId, + metadataToUpdate, + layoutToAdd + ), `Non-unique metadata with ID ${existingId}` ); }); @@ -194,9 +220,13 @@ describe("updateLayout", () => { saveLocalEntity(metadataSaveLocation, [existingMetadata]); saveLocalEntity(layoutsSaveLocation, [existingLayout, existingLayout]); - expectError( + expectPromiseRejectsWithError( () => - persistenceManager.updateLayout(existingId, metadataToAdd, layoutToAdd), + persistenceManager.updateLayout( + existingId, + metadataToUpdate, + layoutToAdd + ), `Non-unique layout with ID ${existingId}` ); }); @@ -205,9 +235,13 @@ describe("updateLayout", () => { saveLocalEntity(metadataSaveLocation, [existingMetadata, existingMetadata]); saveLocalEntity(layoutsSaveLocation, [existingLayout, existingLayout]); - expectError( + expectPromiseRejectsWithError( () => - persistenceManager.updateLayout(existingId, metadataToAdd, layoutToAdd), + persistenceManager.updateLayout( + existingId, + metadataToUpdate, + layoutToAdd + ), `Non-unique metadata with ID ${existingId}; Non-unique layout with ID ${existingId}` ); }); @@ -215,9 +249,13 @@ describe("updateLayout", () => { it("errors if there are multiple metadata entries and no layouts in local storage with requested ID ", async () => { saveLocalEntity(metadataSaveLocation, [existingMetadata, existingMetadata]); - expectError( + expectPromiseRejectsWithError( () => - persistenceManager.updateLayout(existingId, metadataToAdd, layoutToAdd), + persistenceManager.updateLayout( + existingId, + metadataToUpdate, + layoutToAdd + ), `Non-unique metadata with ID ${existingId}; No layout with ID ${existingId}` ); }); @@ -225,9 +263,13 @@ describe("updateLayout", () => { it("errors if there are no metadata entries and multiple layouts in local storage with requested ID ", async () => { saveLocalEntity(layoutsSaveLocation, [existingLayout, existingLayout]); - expectError( + expectPromiseRejectsWithError( () => - persistenceManager.updateLayout(existingId, metadataToAdd, layoutToAdd), + persistenceManager.updateLayout( + existingId, + metadataToUpdate, + layoutToAdd + ), `No metadata with ID ${existingId}; Non-unique layout with ID ${existingId}` ); }); @@ -251,7 +293,7 @@ describe("deleteLayout", () => { it("errors if there is no metadata in local storage with requested ID ", async () => { saveLocalEntity(layoutsSaveLocation, [existingLayout]); - expectError( + expectPromiseRejectsWithError( () => persistenceManager.deleteLayout(existingId), `No metadata with ID ${existingId}` ); @@ -260,7 +302,7 @@ describe("deleteLayout", () => { it("errors if there is no layout in local storage with requested ID ", async () => { saveLocalEntity(metadataSaveLocation, [existingMetadata]); - expectError( + expectPromiseRejectsWithError( () => persistenceManager.deleteLayout(existingId), `No layout with ID ${existingId}` ); @@ -269,7 +311,7 @@ describe("deleteLayout", () => { it("errors if there is no metadata or layout in local storage with requested ID ", async () => { const requestedId = "non_existent_id"; - expectError( + expectPromiseRejectsWithError( () => persistenceManager.deleteLayout(requestedId), `No metadata with ID ${requestedId}; No layout with ID ${requestedId}` ); @@ -279,7 +321,7 @@ describe("deleteLayout", () => { saveLocalEntity(metadataSaveLocation, [existingMetadata, existingMetadata]); saveLocalEntity(layoutsSaveLocation, [existingLayout]); - expectError( + expectPromiseRejectsWithError( () => persistenceManager.deleteLayout(existingId), `Non-unique metadata with ID ${existingId}` ); @@ -289,7 +331,7 @@ describe("deleteLayout", () => { saveLocalEntity(metadataSaveLocation, [existingMetadata]); saveLocalEntity(layoutsSaveLocation, [existingLayout, existingLayout]); - expectError( + expectPromiseRejectsWithError( () => persistenceManager.deleteLayout(existingId), `Non-unique layout with ID ${existingId}` ); @@ -299,7 +341,7 @@ describe("deleteLayout", () => { saveLocalEntity(metadataSaveLocation, [existingMetadata, existingMetadata]); saveLocalEntity(layoutsSaveLocation, [existingLayout, existingLayout]); - expectError( + expectPromiseRejectsWithError( () => persistenceManager.deleteLayout(existingId), `Non-unique metadata with ID ${existingId}; Non-unique layout with ID ${existingId}` ); @@ -308,7 +350,7 @@ describe("deleteLayout", () => { it("errors if there are multiple metadata entries and no layouts in local storage with requested ID ", async () => { saveLocalEntity(metadataSaveLocation, [existingMetadata, existingMetadata]); - expectError( + expectPromiseRejectsWithError( () => persistenceManager.deleteLayout(existingId), `Non-unique metadata with ID ${existingId}; No layout with ID ${existingId}` ); @@ -317,7 +359,7 @@ describe("deleteLayout", () => { it("errors if there are no metadata entries and multiple layouts in local storage with requested ID ", async () => { saveLocalEntity(layoutsSaveLocation, [existingLayout, existingLayout]); - expectError( + expectPromiseRejectsWithError( () => persistenceManager.deleteLayout(existingId), `No metadata with ID ${existingId}; Non-unique layout with ID ${existingId}` ); @@ -345,7 +387,7 @@ describe("loadLayout", () => { it("errors if there is no layout in local storage with requested ID ", async () => { saveLocalEntity(metadataSaveLocation, [existingMetadata]); - expectError( + expectPromiseRejectsWithError( () => persistenceManager.loadLayout(existingId), `No layout with ID ${existingId}` ); @@ -354,7 +396,7 @@ describe("loadLayout", () => { it("errors if there is no metadata or layout in local storage with requested ID ", async () => { const requestedId = "non_existent_id"; - expectError( + expectPromiseRejectsWithError( () => persistenceManager.loadLayout(requestedId), `No layout with ID ${requestedId}` ); @@ -373,7 +415,7 @@ describe("loadLayout", () => { saveLocalEntity(metadataSaveLocation, [existingMetadata]); saveLocalEntity(layoutsSaveLocation, [existingLayout, existingLayout]); - expectError( + expectPromiseRejectsWithError( () => persistenceManager.loadLayout(existingId), `Non-unique layout with ID ${existingId}` ); @@ -383,7 +425,7 @@ describe("loadLayout", () => { saveLocalEntity(metadataSaveLocation, [existingMetadata, existingMetadata]); saveLocalEntity(layoutsSaveLocation, [existingLayout, existingLayout]); - expectError( + expectPromiseRejectsWithError( () => persistenceManager.loadLayout(existingId), `Non-unique layout with ID ${existingId}` ); @@ -392,7 +434,7 @@ describe("loadLayout", () => { it("errors if there are multiple metadata entries and no layouts in local storage with requested ID ", async () => { saveLocalEntity(metadataSaveLocation, [existingMetadata, existingMetadata]); - expectError( + expectPromiseRejectsWithError( () => persistenceManager.loadLayout(existingId), `No layout with ID ${existingId}` ); @@ -401,7 +443,7 @@ describe("loadLayout", () => { it("errors if there are no metadata entries and multiple layouts in local storage with requested ID ", async () => { saveLocalEntity(layoutsSaveLocation, [existingLayout, existingLayout]); - expectError( + expectPromiseRejectsWithError( () => persistenceManager.loadLayout(existingId), `Non-unique layout with ID ${existingId}` ); @@ -429,7 +471,3 @@ describe("loadMetadata", () => { expect(await persistenceManager.loadMetadata()).toEqual([]); }); }); - -const expectError = (f: () => Promise, message: string) => { - expect(f).rejects.toStrictEqual(new Error(message)); -}; 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..213d1e6a9 --- /dev/null +++ b/vuu-ui/packages/vuu-layout/test/layout-persistence/RemoteLayoutPersistenceManager.test.ts @@ -0,0 +1,304 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { + GetLayoutResponseDto, + CreateLayoutResponseDto, + RemoteLayoutPersistenceManager, +} from "../../src/layout-persistence/RemoteLayoutPersistenceManager"; +import { LayoutMetadata, LayoutMetadataDto } 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: "screenshot", + user: "username", + created: "01.01.2000", +}; + +const metadataToAdd: LayoutMetadataDto = { + name: "layout 1", + group: "group 1", + screenshot: "screenshot", + user: "username", +}; + +const layout: LayoutJSON = { + 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; +}; + +describe("RemoteLayoutPersistenceManager", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe("createLayout", () => { + const responseJSON: CreateLayoutResponseDto = { + metadata: { + ...metadataToAdd, + id: uniqueId, + created: dateString, + }, + }; + + it("resolves with metadata when fetch resolves, response is ok and contains metadata", () => { + const fetchResponse: 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: FetchResponse = { + json: () => new Promise((resolve) => resolve(responseJSON)), + ok: false, + statusText: errorMessage, + }; + + mockFetch.mockResolvedValue(fetchResponse); + + expectPromiseRejectsWithError( + () => persistence.createLayout(metadata, layout), + errorMessage + ); + }); + + it("rejects with error when metadata in response is falsey", () => { + const fetchResponse: FetchResponse = { + json: () => new Promise((resolve) => resolve({})), + ok: true, + }; + + mockFetch.mockResolvedValue(fetchResponse); + + expectPromiseRejectsWithError( + () => persistence.createLayout(metadata, layout), + "Response did not contain valid metadata" + ); + }); + + it("rejects with error when fetch rejects", () => { + mockFetch.mockRejectedValue(fetchError); + + expectPromiseRejectsWithError( + () => persistence.createLayout(metadata, layout), + fetchError.message + ); + }); + }); + + describe("updateLayout", () => { + it("resolves when fetch resolves and response is ok", () => { + const fetchResponse: FetchResponse = { + ok: true, + }; + + mockFetch.mockResolvedValue(fetchResponse); + + const result = persistence.updateLayout(uniqueId, metadata, layout); + + expect(result).resolves.toBe(undefined); + }); + + it("rejects with error when response is not ok", () => { + const errorMessage = "Not Found"; + + const fetchResponse: FetchResponse = { + ok: false, + statusText: errorMessage, + }; + + mockFetch.mockResolvedValue(fetchResponse); + + expectPromiseRejectsWithError( + () => persistence.updateLayout(uniqueId, metadata, layout), + errorMessage + ); + }); + + it("rejects with error when fetch rejects", () => { + mockFetch.mockRejectedValue(fetchError); + + expectPromiseRejectsWithError( + () => persistence.updateLayout(uniqueId, metadata, layout), + fetchError.message + ); + }); + }); + + describe("deleteLayout", () => { + it("resolves when fetch resolves and response is ok", () => { + const fetchResponse: 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: 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 responseJson = [metadata]; + + const fetchResponse: FetchResponse = { + json: () => new Promise((resolve) => resolve(responseJson)), + ok: true, + }; + + mockFetch.mockResolvedValue(fetchResponse); + + const result = persistence.loadMetadata(); + + expect(result).resolves.toBe(responseJson); + }); + + it("rejects with error when response is not ok", () => { + const errorMessage = "Not Found"; + + const fetchResponse: FetchResponse = { + json: () => new Promise((resolve) => resolve()), + ok: false, + statusText: errorMessage, + }; + + mockFetch.mockResolvedValue(fetchResponse); + + expectPromiseRejectsWithError( + () => persistence.loadMetadata(), + errorMessage + ); + }); + + it("rejects with error when metadata is falsey in response", () => { + const fetchResponse: FetchResponse = { + json: () => new Promise((resolve) => resolve()), + ok: true, + }; + + mockFetch.mockResolvedValue(fetchResponse); + + expectPromiseRejectsWithError( + () => persistence.loadMetadata(), + "Response did not contain valid metadata" + ); + }); + + it("rejects with error when fetch rejects", () => { + mockFetch.mockRejectedValue(fetchError); + + expectPromiseRejectsWithError( + () => persistence.loadMetadata(), + fetchError.message + ); + }); + }); + + describe("loadLayout", () => { + it("resolves with array of metadata when response is ok", () => { + const fetchResponse: FetchResponse = { + json: () => new Promise((resolve) => resolve({ definition: 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 errorMessage = "Not Found"; + + const fetchResponse: FetchResponse = { + json: () => new Promise((resolve) => resolve({})), + ok: false, + statusText: errorMessage, + }; + + mockFetch.mockResolvedValue(fetchResponse); + + expectPromiseRejectsWithError( + () => persistence.loadLayout(uniqueId), + errorMessage + ); + }); + + it("rejects with error when definition is falsey in response", () => { + const fetchResponse: FetchResponse = { + json: () => new Promise((resolve) => resolve({})), + ok: true, + }; + + mockFetch.mockResolvedValue(fetchResponse); + + expectPromiseRejectsWithError( + () => persistence.loadLayout(uniqueId), + "Response did not contain a valid layout" + ); + }); + + it("rejects with error when fetch rejects", () => { + 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-popups/src/dialog-header/DialogHeader.css b/vuu-ui/packages/vuu-popups/src/dialog-header/DialogHeader.css index d7d487ca3..6877e228f 100644 --- a/vuu-ui/packages/vuu-popups/src/dialog-header/DialogHeader.css +++ b/vuu-ui/packages/vuu-popups/src/dialog-header/DialogHeader.css @@ -3,23 +3,23 @@ --saltButton-width: 28px; --saltToolbar-background: transparent; --saltToolbar-height: calc(var(--salt-size-base) + 5px); + --vuuToolbarProxy-height: 22px; + --salt-text-fontFamily: Nunito Sans A-Variant, sans-serif; - align-items: flex-start; display: flex; + align-items: flex-start; color: var(--light-text-primary, #15171B); font-feature-settings: 'ss02' on, 'ss01' on, 'salt' on, 'liga' off; font-size: 16px; font-weight: 600; - padding: 0 6px; - } - .vuuDialogHeader > .Responsive-inner { +.vuuDialogHeader > .Responsive-inner { align-items: center; - } +} - .vuuDialogHeader > .Responsive-inner > :last-child{ +.vuuDialogHeader > .Responsive-inner > :last-child{ right: 2px; - } +} \ No newline at end of file diff --git a/vuu-ui/packages/vuu-popups/src/dialog/Dialog.css b/vuu-ui/packages/vuu-popups/src/dialog/Dialog.css index 15a253dab..e78d547bd 100644 --- a/vuu-ui/packages/vuu-popups/src/dialog/Dialog.css +++ b/vuu-ui/packages/vuu-popups/src/dialog/Dialog.css @@ -2,9 +2,13 @@ background: var(--salt-container-primary-background); border: var(--vuuDialog-border, solid 1px #ccc); border-radius: 5px; - padding: var(--vuuDialog-padding, 0); + padding: var(--vuuDialog-padding, 16px); box-shadow: var(--salt-overlayable-shadow, none); height: var(--vuuDialog-height, fit-content); width: var(--vuuDialog-width, fit-content); } +.vuuDialog-body { + padding-top: 16px; +} + diff --git a/vuu-ui/packages/vuu-popups/src/dialog/Dialog.tsx b/vuu-ui/packages/vuu-popups/src/dialog/Dialog.tsx index 5d4dff0ec..6338c682d 100644 --- a/vuu-ui/packages/vuu-popups/src/dialog/Dialog.tsx +++ b/vuu-ui/packages/vuu-popups/src/dialog/Dialog.tsx @@ -42,7 +42,7 @@ export const Dialog = ({ onClose={close} title={title} /> - {children} +
{children}
diff --git a/vuu-ui/packages/vuu-popups/src/dialog/useDialog.tsx b/vuu-ui/packages/vuu-popups/src/dialog/useDialog.tsx index 1e18c6aa9..582f68a10 100644 --- a/vuu-ui/packages/vuu-popups/src/dialog/useDialog.tsx +++ b/vuu-ui/packages/vuu-popups/src/dialog/useDialog.tsx @@ -4,6 +4,7 @@ import { Dialog } from "./Dialog"; export type DialogState = { content: ReactElement; title: string; + hideCloseButton?: boolean; }; export type SetDialog = (dialogState?: DialogState) => void; @@ -22,6 +23,7 @@ export const useDialog = () => { onClose={handleClose} style={{ maxHeight: 500 }} title={dialogState.title} + hideCloseButton={dialogState.hideCloseButton} > {dialogState.content} diff --git a/vuu-ui/packages/vuu-shell/src/layout-management/LayoutList.tsx b/vuu-ui/packages/vuu-shell/src/layout-management/LayoutList.tsx index 3cf44170c..82721ccd5 100644 --- a/vuu-ui/packages/vuu-shell/src/layout-management/LayoutList.tsx +++ b/vuu-ui/packages/vuu-shell/src/layout-management/LayoutList.tsx @@ -41,26 +41,26 @@ export const LayoutsList = (props: HTMLAttributes) => { source={Object.entries(layoutsByGroup)} ListItem={({ item }) => { if (!item) return <>; - const [groupName, layouts] = item; + const [groupName, layoutMetadata] = item; return ( <>
{groupName}
- {layouts.map((layout) => ( + {layoutMetadata.map((metadata) => (
handleLoadLayout(layout?.id)} + key={metadata?.id} + onClick={() => handleLoadLayout(metadata?.id)} >
- {layout?.name} + {metadata?.name}
-
{`${layout?.user}, ${layout?.date}`}
+
{`${metadata?.user}, ${metadata?.created}`}
diff --git a/vuu-ui/packages/vuu-shell/src/layout-management/SaveLayoutPanel.css b/vuu-ui/packages/vuu-shell/src/layout-management/SaveLayoutPanel.css index 81ab92060..f7c7a3fde 100644 --- a/vuu-ui/packages/vuu-shell/src/layout-management/SaveLayoutPanel.css +++ b/vuu-ui/packages/vuu-shell/src/layout-management/SaveLayoutPanel.css @@ -6,14 +6,13 @@ --salt-text-label-fontSize: 10px; --saltFormFieldLegacy-label-paddingLeft: 0; --saltFormField-label-fontWeight: 400; + --saltText-color: var(--text-secondary-foreground, #606477); } .saveLayoutPanel-panelContainer { display: flex; - padding: 16px; flex-direction: column; align-items: flex-start; - gap: 16px; } .saveLayoutPanel-panelContent { @@ -37,13 +36,13 @@ } .saveLayoutPanel-inputText { - color: var(--light-text-secondary, #606477); - font-family: Nunito Sans Regular; + border: none; + color: var(--light-text-primary, #15171B); + font-family: Nunito Sans A-Variant, serif; font-feature-settings: 'ss02' on, 'ss01' on, 'salt' on, 'liga' off; font-size: 12px; font-weight: 400; line-height: 16px; - border: none; padding-left: 4px; width: 100%; outline: none; @@ -81,11 +80,11 @@ .saveLayoutPanel-buttonsContainer { display: flex; - padding-top: 8px; justify-content: flex-end; align-items: flex-start; - gap: 8px; align-self: stretch; + padding-top: 24px; + gap: 8px; } .saveLayoutPanel-cancelButton, @@ -96,7 +95,6 @@ align-items: flex-start; gap: 8px; border-radius: 6px; - font-feature-settings: 'ss02' on, 'ss01' on, 'salt' on, 'liga' off; font-size: 12px; font-style: normal; font-weight: 700; 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 513a5d05f..b0cf1cc54 100644 --- a/vuu-ui/packages/vuu-shell/src/layout-management/SaveLayoutPanel.tsx +++ b/vuu-ui/packages/vuu-shell/src/layout-management/SaveLayoutPanel.tsx @@ -1,8 +1,8 @@ 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 { LayoutMetadata } from "./layoutTypes"; +import { takeScreenshot } from "@finos/vuu-utils"; +import { LayoutMetadataDto } from "./layoutTypes"; import "./SaveLayoutPanel.css"; @@ -19,8 +19,8 @@ type RadioValue = (typeof radioValues)[number]; type SaveLayoutPanelProps = { onCancel: () => void; - onSave: (layoutMetadata: Omit) => void; - componentId?: string; + onSave: (layoutMetadata: LayoutMetadataDto) => void; + componentId?: string }; export const SaveLayoutPanel = (props: SaveLayoutPanelProps) => { @@ -45,10 +45,9 @@ export const SaveLayoutPanel = (props: SaveLayoutPanelProps) => { name: layoutName, group, screenshot: screenshot ?? "", - user: "User", - date: formatDate(new Date(), "dd.mm.yyyy"), - }); - }; + user: "User" + }) + } return (
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..9e264f494 100644 --- a/vuu-ui/packages/vuu-shell/src/layout-management/layoutTypes.ts +++ b/vuu-ui/packages/vuu-shell/src/layout-management/layoutTypes.ts @@ -9,9 +9,16 @@ export interface LayoutMetadata extends WithId { group: string; screenshot: string; user: string; - date: string; + created: string; } +export type LayoutMetadataDto = Omit; + export interface Layout extends WithId { json: LayoutJSON; } + +export type ApplicationLayout = { + username: string, + definition: 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 9785c7c6a..060d41a31 100644 --- a/vuu-ui/packages/vuu-shell/src/layout-management/useLayoutManager.tsx +++ b/vuu-ui/packages/vuu-shell/src/layout-management/useLayoutManager.tsx @@ -1,31 +1,34 @@ import React, { - useState, useCallback, useContext, useEffect, useRef, + useState, } from "react"; import { defaultLayout, LayoutJSON, LayoutPersistenceManager, LocalLayoutPersistenceManager, + RemoteLayoutPersistenceManager, resolveJSONPath, } from "@finos/vuu-layout"; -import { LayoutMetadata } from "./layoutTypes"; +import { LayoutMetadata, LayoutMetadataDto } from "./layoutTypes"; let _persistenceManager: LayoutPersistenceManager; const getPersistenceManager = () => { if (_persistenceManager === undefined) { - _persistenceManager = new LocalLayoutPersistenceManager(); + _persistenceManager = process.env.LOCAL + ? new LocalLayoutPersistenceManager() + : new RemoteLayoutPersistenceManager(); } return _persistenceManager; }; 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; @@ -62,40 +65,56 @@ export const LayoutManagementProvider = ( useEffect(() => { const persistenceManager = getPersistenceManager(); - persistenceManager.loadMetadata().then((metadata) => { - setLayoutMetadata(metadata); - }); - persistenceManager.loadApplicationLayout().then((layout) => { - setApplicationLayout(layout); - }); + + persistenceManager + .loadMetadata() + .then((metadata) => { + setLayoutMetadata(metadata); + }) + .catch((error: Error) => { + //TODO: Show error toaster + console.error("Error occurred while retrieving metadata", error); + }); + + persistenceManager + .loadApplicationLayout() + .then((layout: LayoutJSON) => { + setApplicationLayout(layout); + }) + .catch((error: Error) => { + //TODO: Show error toaster + console.error( + "Error occurred while retrieving application layout", + error + ); + }); }, [setApplicationLayout]); const saveApplicationLayout = useCallback( (layout: LayoutJSON) => { - const persistenceManager = getPersistenceManager(); + console.log(`save application layout ${JSON.stringify(layout, null, 2)}`); setApplicationLayout(layout, false); - persistenceManager.saveApplicationLayout(layout); + getPersistenceManager().saveApplicationLayout(layout); }, [setApplicationLayout] ); - const saveLayout = useCallback((metadata: Omit) => { + const saveLayout = useCallback((metadata: LayoutMetadataDto) => { const layoutToSave = resolveJSONPath( applicationLayoutRef.current, "#main-tabs.ACTIVE_CHILD" ); if (layoutToSave) { - const persistenceManager = getPersistenceManager(); - persistenceManager + getPersistenceManager() .createLayout(metadata, layoutToSave) - .then((generatedId) => { - const newMetadata: LayoutMetadata = { - ...metadata, - id: generatedId, - }; - - setLayoutMetadata((prev) => [...prev, newMetadata]); + .then((metadata) => { + //TODO: Show success toast + setLayoutMetadata((prev) => [...prev, metadata]); + }) + .catch((error: Error) => { + //TODO: Show error toaster + console.error("Error occurred while saving layout", error); }); } //TODO else{ show error message} @@ -103,15 +122,16 @@ export const LayoutManagementProvider = ( const loadLayoutById = useCallback( (id: string) => { - const persistenceManager = getPersistenceManager(); - persistenceManager.loadLayout(id).then((layoutJson) => { - const { current: prev } = applicationLayoutRef; - setApplicationLayout({ - ...prev, - active: prev.children?.length ?? 0, - children: [...(prev.children || []), layoutJson], + getPersistenceManager() + .loadLayout(id) + .then((layoutJson) => { + const { current: prev } = applicationLayoutRef; + setApplicationLayout({ + ...prev, + active: prev.children?.length ?? 0, + children: [...(prev.children || []), layoutJson], + }); }); - }); }, [setApplicationLayout] ); diff --git a/vuu-ui/packages/vuu-theme/NunitoSans-Regular.woff b/vuu-ui/packages/vuu-theme/fonts/NunitoSans-Regular.woff similarity index 100% rename from vuu-ui/packages/vuu-theme/NunitoSans-Regular.woff rename to vuu-ui/packages/vuu-theme/fonts/NunitoSans-Regular.woff diff --git a/vuu-ui/packages/vuu-theme/fonts/NunitoSans.css b/vuu-ui/packages/vuu-theme/fonts/NunitoSans.css index 5ede2aac8..023d0ba13 100644 --- a/vuu-ui/packages/vuu-theme/fonts/NunitoSans.css +++ b/vuu-ui/packages/vuu-theme/fonts/NunitoSans.css @@ -7,7 +7,9 @@ font-display: swap; src: url(./NunitoSansv15Latin.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 */ +} + +/* latin */ @font-face { font-family: 'Nunito Sans'; font-style: normal; @@ -16,7 +18,9 @@ font-display: swap; src: url(./NunitoSansv15Latin.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 */ +} + +/* latin */ @font-face { font-family: 'Nunito Sans'; font-style: normal; @@ -26,7 +30,8 @@ src: url(./NunitoSansv15Latin.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 */ + +/* latin */ @font-face { font-family: 'Nunito Sans'; font-style: normal; @@ -36,23 +41,31 @@ src: url(./NunitoSansv15Latin.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(./NunitoSansv15Latin.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(./NunitoSansv15Latin.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 + +/* latin */ +@font-face { + font-family: 'Nunito Sans'; + font-style: normal; + font-weight: 700; + font-stretch: 100%; + font-display: swap; + src: url(./NunitoSansv15Latin.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(./NunitoSansv15Latin.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; +} + +/* This font contains the variation of the 'a' character, which can be applied with `font-feature-settings` as per Figma */ +@font-face { + font-family: 'Nunito Sans A-Variant'; + src: url('./NunitoSans-Regular.woff') format('opentype'); +} diff --git a/vuu-ui/packages/vuu-ui-controls/src/inputs/Checkbox.css b/vuu-ui/packages/vuu-ui-controls/src/inputs/Checkbox.css index 39af11346..713cddbad 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/inputs/Checkbox.css +++ b/vuu-ui/packages/vuu-ui-controls/src/inputs/Checkbox.css @@ -5,6 +5,7 @@ align-items: center; gap: 6px; color: var(--light-text-primary, #15171B); + font-family: Nunito Sans A-Variant, serif; font-feature-settings: 'ss02' on, 'ss01' on, 'salt' on, 'liga' off; font-size: 12px; font-weight: 400; diff --git a/vuu-ui/packages/vuu-ui-controls/src/inputs/RadioButton.css b/vuu-ui/packages/vuu-ui-controls/src/inputs/RadioButton.css index 26526a7fc..7f11e7945 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/inputs/RadioButton.css +++ b/vuu-ui/packages/vuu-ui-controls/src/inputs/RadioButton.css @@ -4,6 +4,7 @@ align-items: center; gap: 6px; color: var(--light-text-primary, #15171B); + font-family: Nunito Sans A-Variant, sans-serif; font-feature-settings: 'ss02' on, 'ss01' on, 'salt' on, 'liga' off; font-size: 12px; font-weight: 400; diff --git a/vuu-ui/sample-apps/feature-basket-trading/src/new-basket-panel/NewBasketPanel.css b/vuu-ui/sample-apps/feature-basket-trading/src/new-basket-panel/NewBasketPanel.css index e6116a0f9..d6ece0fbe 100644 --- a/vuu-ui/sample-apps/feature-basket-trading/src/new-basket-panel/NewBasketPanel.css +++ b/vuu-ui/sample-apps/feature-basket-trading/src/new-basket-panel/NewBasketPanel.css @@ -8,6 +8,7 @@ .vuuBasketNewBasketPanel-body { flex: 1 1 auto; + padding: 16px 0; } .vuuBasketNewBasketPanel-buttonBar { @@ -15,9 +16,7 @@ --saltButton-padding: 12px; display: flex; align-items: flex-end; - gap: 8px; - height: 32px; justify-content: flex-end; - padding: 0 16px; - -} \ No newline at end of file + padding-top: 8px; + gap: 8px; +} diff --git a/vuu-ui/sample-apps/feature-basket-trading/src/new-basket-panel/NewBasketPanel.tsx b/vuu-ui/sample-apps/feature-basket-trading/src/new-basket-panel/NewBasketPanel.tsx index 5076a3ae4..a205b28bd 100644 --- a/vuu-ui/sample-apps/feature-basket-trading/src/new-basket-panel/NewBasketPanel.tsx +++ b/vuu-ui/sample-apps/feature-basket-trading/src/new-basket-panel/NewBasketPanel.tsx @@ -81,7 +81,11 @@ export const NewBasketPanel = ({
- +
Basket Name diff --git a/vuu-ui/scripts/esbuild.mjs b/vuu-ui/scripts/esbuild.mjs index 392ea30a7..254020925 100644 --- a/vuu-ui/scripts/esbuild.mjs +++ b/vuu-ui/scripts/esbuild.mjs @@ -26,6 +26,8 @@ export async function build(config) { define: { "process.env.NODE_ENV": `"${env}"`, "process.env.NODE_DEBUG": `false`, + "process.env.LOCAL": `true`, + "process.env.LAYOUT_BASE_URL": `"http://127.0.0.1:8081/api"`, }, external, footer, diff --git a/vuu-ui/showcase/vite.config.js b/vuu-ui/showcase/vite.config.js index 81ce7f139..a54edf443 100644 --- a/vuu-ui/showcase/vite.config.js +++ b/vuu-ui/showcase/vite.config.js @@ -8,6 +8,8 @@ export default defineConfig({ }, define: { "process.env.NODE_DEBUG": false, + "process.env.LOCAL": true, + "process.env.LAYOUT_BASE_URL": `"http://127.0.0.1:8081/api"`, }, esbuild: { jsx: `automatic`, diff --git a/vuu-ui/tsconfig.json b/vuu-ui/tsconfig.json index 028e09c31..c9c0d8494 100644 --- a/vuu-ui/tsconfig.json +++ b/vuu-ui/tsconfig.json @@ -4,7 +4,12 @@ "noImplicitAny": true, "target": "es2016", "downlevelIteration": true, - "lib": ["dom", "dom.iterable", "esnext", "WebWorker"], + "lib": [ + "dom", + "dom.iterable", + "esnext", + "WebWorker" + ], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, @@ -20,14 +25,17 @@ "sourceMap": true }, "include": [ - "packages/*/src", - "sample-apps/*/src", - "showcase/src", + "packages/*/src", + "sample-apps/*/src", + "showcase/src", "sample-apps/*/index.ts", "sample-apps/*/src", "global.d.ts" , "packages/vuu-data-test/src/UpdateGenerator.ts" ], - "exclude": ["**/*.cy.*", "**/*.test.*"], + "exclude": [ + "**/*.cy.*", + "**/*.test.*" + ], "references": [ {"path" : "packages/vuu-codemirror"}, {"path" : "packages/vuu-data"}, diff --git a/vuu/src/main/scala/org/finos/vuu/provider/VuuJoinTableProvider.scala b/vuu/src/main/scala/org/finos/vuu/provider/VuuJoinTableProvider.scala index 11ce14c0f..dd3dcb62b 100644 --- a/vuu/src/main/scala/org/finos/vuu/provider/VuuJoinTableProvider.scala +++ b/vuu/src/main/scala/org/finos/vuu/provider/VuuJoinTableProvider.scala @@ -1,12 +1,12 @@ package org.finos.vuu.provider import com.typesafe.scalalogging.StrictLogging -import org.finos.vuu.api.{JoinTableDef, TableDef} -import org.finos.vuu.core.table.{DataTable, JoinTable, JoinTableUpdate, RowWithData} -import org.finos.vuu.provider.join.{JoinDefToJoinTable, JoinManagerEventDataSink, JoinRelations, RightToLeftKeys} import org.finos.toolbox.jmx.MetricsProvider import org.finos.toolbox.lifecycle.LifecycleContainer import org.finos.toolbox.time.Clock +import org.finos.vuu.api.{JoinTableDef, TableDef} +import org.finos.vuu.core.table.{DataTable, JoinTable, JoinTableUpdate, RowWithData} +import org.finos.vuu.provider.join.{JoinDefToJoinTable, JoinManagerEventDataSink, JoinRelations, RightToLeftKeys} import java.util import java.util.concurrent.{ArrayBlockingQueue, ConcurrentHashMap}