Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VUU54: Add validation for layout/metadata IDs #60

Merged
merged 18 commits into from
Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,23 @@ export interface LayoutPersistenceManager {
*
* @returns Unique identifier assigned to the saved layout
*/
createLayout: (metadata: Omit<LayoutMetadata, "id">, layout: LayoutJSON) => string;
createLayout: (metadata: Omit<LayoutMetadata, "id">, layout: LayoutJSON) => Promise<string>;

/**
* Overwrites an existing layout and its corresponding metadata with the provided infromation
* Overwrites an existing layout and its corresponding metadata with the provided information
*
* @param id - Unique identifier of the existing layout to be updated
* @param metadata - Metadata describing the new layout to overwrite with
* @param layout - Full JSON representation of the new layout to overwrite with
*/
updateLayout: (id: string, metadata: Omit<LayoutMetadata, "id">, layout: LayoutJSON) => void;
updateLayout: (id: string, metadata: Omit<LayoutMetadata, "id">, layout: LayoutJSON) => Promise<void>;

/**
* Deletes an existing layout and its corresponding metadata
*
* @param id - Unique identifier of the existing layout to be deleted
*/
deleteLayout: (id: string) => void;
deleteLayout: (id: string) => Promise<void>;

/**
* Retrieves an existing layout
Expand All @@ -35,12 +35,12 @@ export interface LayoutPersistenceManager {
*
* @returns Full JSON representation of the layout corresponding to the provided ID
*/
loadLayout: (id: string) => LayoutJSON;
loadLayout: (id: string) => Promise<LayoutJSON>;

/**
* Retrieves metadata for all existing layouts
*
* @returns an array of all persisted layout metadata
*/
loadMetadata: () => LayoutMetadata[];
loadMetadata: () => Promise<LayoutMetadata[]>;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Layout, LayoutMetadata } from "@finos/vuu-shell";
import { Layout, LayoutMetadata, WithId } from "@finos/vuu-shell";
import { LayoutJSON, LayoutPersistenceManager } from "@finos/vuu-layout";

import { getLocalEntity, saveLocalEntity } from "@finos/vuu-filters";
Expand All @@ -8,72 +8,142 @@ const metadataSaveLocation = "layouts/metadata";
const layoutsSaveLocation = "layouts/layouts";

export class LocalLayoutPersistenceManager implements LayoutPersistenceManager {
createLayout(metadata: Omit<LayoutMetadata, "id">, layout: LayoutJSON): string {
console.log(`Saving layout as ${metadata.name} to group ${metadata.group}...`);

const existingLayouts = this.loadLayouts();
const existingMetadata = this.loadMetadata();

const id = getUniqueId();

this.appendAndPersist(id, metadata, layout, existingLayouts, existingMetadata);

return id;
createLayout(metadata: Omit<LayoutMetadata, "id">, layout: LayoutJSON): Promise<string> {
return new Promise(resolve => {
console.log(`Saving layout as ${metadata.name} to group ${metadata.group}...`);

Promise.all([this.loadLayouts(), this.loadMetadata()])
.then(([existingLayouts, existingMetadata]) => {
const id = getUniqueId();
this.appendAndPersist(
id,
metadata,
layout,
existingLayouts,
existingMetadata
);
resolve(id);
});
})
}

updateLayout(id: string, metadata: Omit<LayoutMetadata, "id">, newLayoutJson: LayoutJSON): void {
const existingLayouts = this.loadLayouts().filter(layout => layout.id !== id);
const existingMetadata = this.loadMetadata().filter(metadata => metadata.id !== id);

this.appendAndPersist(id, metadata, newLayoutJson, existingLayouts, existingMetadata);
updateLayout(
id: string,
newMetadata: Omit<LayoutMetadata, "id">,
newLayout: LayoutJSON
): Promise<void> {
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);
this.appendAndPersist(id, newMetadata, newLayout, layouts, metadata);
resolve();
})
.catch(e => reject(e));
});
pling-scottlogic marked this conversation as resolved.
Show resolved Hide resolved
}

deleteLayout(id: string): void {
const layouts = this.loadLayouts().filter(layout => layout.id !== id);
const metadata = this.loadMetadata().filter(metadata => metadata.id !== id);

this.saveLayoutsWithMetadata(layouts, metadata);
deleteLayout(id: string): Promise<void> {
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);
this.saveLayoutsWithMetadata(layouts, metadata);
resolve();
})
.catch(e => reject(e));
});
}

loadLayout(id: string): LayoutJSON {
const layout = this.loadLayouts().filter(layout => layout.id === id);

switch (layout.length) {
case 1: {
return layout[0].json;
}
case 0: {
console.log(`WARNING: no layout exists for ID "${id}"; returning empty layout`);
return {} as LayoutJSON;
}
default: {
console.log(`WARNING: multiple layouts exist for ID "${id}"; returning first instance`)
return layout[0].json;
}
}
loadLayout(id: string): Promise<LayoutJSON> {
return new Promise((resolve, reject) => {
this.validateId(id, "layout")
.then(() => this.loadLayouts())
.then(existingLayouts => {
const layouts = existingLayouts.find(layout => layout.id === id) as Layout;
resolve(layouts.json);
})
.catch(e => reject(e));
});
}

loadMetadata(): LayoutMetadata[] {
return getLocalEntity<LayoutMetadata[]>(metadataSaveLocation) || [];
loadMetadata(): Promise<LayoutMetadata[]> {
return new Promise(resolve => {
const metadata = getLocalEntity<LayoutMetadata[]>(metadataSaveLocation);
resolve(metadata || []);
})
}

private loadLayouts(): Layout[] {
return getLocalEntity<Layout[]>(layoutsSaveLocation) || [];
private loadLayouts(): Promise<Layout[]> {
return new Promise(resolve => {
const layouts = getLocalEntity<Layout[]>(layoutsSaveLocation);
resolve(layouts || []);
});
}

private appendAndPersist(newId: string,
newMetadata: Omit<LayoutMetadata, "id">,
newLayout: LayoutJSON,
existingLayouts: Layout[],
existingMetadata: LayoutMetadata[]) {
existingLayouts.push({id: newId, json: newLayout});
existingMetadata.push({id: newId, ...newMetadata});
private appendAndPersist(
newId: string,
newMetadata: Omit<LayoutMetadata, "id">,
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[]): void {
private saveLayoutsWithMetadata(
layouts: Layout[],
metadata: LayoutMetadata[]
): void {
saveLocalEntity<Layout[]>(layoutsSaveLocation, layouts);
saveLocalEntity<LayoutMetadata[]>(metadataSaveLocation, metadata);
}

// Ensures that there is exactly one Layout entry and exactly one Metadata
// entry in local storage corresponding to the provided ID.
private async validateIds(id: string): Promise<void> {
return Promise
vferraro-scottlogic marked this conversation as resolved.
Show resolved Hide resolved
.all([
this.validateId(id, "metadata").catch(error => error.message),
vferraro-scottlogic marked this conversation as resolved.
Show resolved Hide resolved
this.validateId(id, "layout").catch(error => error.message)
])
.then((errorMessages: string[]) => {
// filter() is used to remove any blank messages before joining.
// Avoids orphaned delimiters in combined messages, e.g. "; " or "; error 2"
const combinedMessage = errorMessages.filter(msg => msg !== undefined).join("; ");
if (combinedMessage) {
throw new Error(combinedMessage);
}
});
}

// Ensures that there is exactly one element (Layout or Metadata) in local
// storage corresponding to the provided ID.
private validateId(id: string, dataType: "metadata" | "layout"): Promise<void> {
return new Promise((resolve, reject) => {
const loadFunc = dataType === "metadata" ? this.loadMetadata : this.loadLayouts;

loadFunc().then((array: WithId[]) => {
const count = array.filter(element => element.id === id).length;
switch (count) {
case 1: {
resolve();
break;
}
case 0: {
reject(new Error(`No ${dataType} with ID ${id}`));
break;
}
default: reject(new Error(`Non-unique ${dataType} with ID ${id}`));
}
});
})
}
}
14 changes: 14 additions & 0 deletions vuu-ui/packages/vuu-layout/src/layout-persistence/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export const warningLayout = {
type: "View",
props: {
style: { height: "calc(100% - 6px)" },
},
children: [
{
props: {
className: "vuuShell-warningPlaceholder",
},
type: "Placeholder",
},
],
};
Loading
Loading