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

[Large] Create logic for editing metadata #372

Open
wants to merge 26 commits into
base: feature/metadata-editing/develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f8db60c
Start work on MMS endpoint
aswallace Oct 4, 2024
3a56a6a
Add edit files logic and support in fileservices
SeanLeRoy Oct 4, 2024
a669973
Update to use FES stored annotationID
SeanLeRoy Oct 7, 2024
d405170
Update comment
SeanLeRoy Oct 7, 2024
8eafa0f
Remove MMS
SeanLeRoy Oct 7, 2024
f4a1eb9
Map FES base URL to MMS base url
SeanLeRoy Oct 7, 2024
d39f514
Change minor spelling
aswallace Oct 15, 2024
a1392ba
Fix editFileLogic tests
aswallace Oct 16, 2024
64d8bad
Start work on MMS endpoint
aswallace Oct 4, 2024
47cbad7
Add edit files logic and support in fileservices
SeanLeRoy Oct 4, 2024
d773efb
Update to use FES stored annotationID
SeanLeRoy Oct 7, 2024
d249446
Remove MMS
SeanLeRoy Oct 7, 2024
90dcd75
Map FES base URL to MMS base url
SeanLeRoy Oct 7, 2024
a528766
Change minor spelling
aswallace Oct 15, 2024
18ce082
Start work on MMS endpoint
aswallace Oct 4, 2024
97d6710
Add edit files logic and support in fileservices
SeanLeRoy Oct 4, 2024
4bb3c33
Update to use FES stored annotationID
SeanLeRoy Oct 7, 2024
6f66c59
Remove MMS
SeanLeRoy Oct 7, 2024
85ecfe2
Map FES base URL to MMS base url
SeanLeRoy Oct 7, 2024
55649ba
Change minor spelling
aswallace Oct 15, 2024
b0e6544
temporarily skip out-of-date tests
aswallace Dec 10, 2024
2d42179
add refresh for edits and account for annotations with spaces
aswallace Dec 17, 2024
4b8da5e
update file id references to match UID changes
aswallace Dec 18, 2024
80d9536
fix MMS endpoint base urls and request body
aswallace Dec 19, 2024
e885706
remove logic for getting metadata from MMS
aswallace Dec 19, 2024
e5114f8
trim value strings to prevent empty submits
aswallace Dec 20, 2024
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
@@ -1,10 +1,12 @@
import { IComboBoxOption } from "@fluentui/react";
import classNames from "classnames";
import * as React from "react";
import { useDispatch } from "react-redux";

import MetadataDetails, { ValueCountItem } from "./MetadataDetails";
import { PrimaryButton, SecondaryButton } from "../Buttons";
import ComboBox from "../ComboBox";
import { interaction } from "../../state";

import styles from "./EditMetadata.module.css";

Expand All @@ -20,6 +22,7 @@ interface ExistingAnnotationProps {
* and then entering values for the selected files
*/
export default function ExistingAnnotationPathway(props: ExistingAnnotationProps) {
const dispatch = useDispatch();
const [newValues, setNewValues] = React.useState<string>();
const [valueCount, setValueCount] = React.useState<ValueCountItem[]>();
const [selectedAnnotation, setSelectedAnnotation] = React.useState<string | undefined>();
Expand Down Expand Up @@ -65,7 +68,9 @@ export default function ExistingAnnotationPathway(props: ExistingAnnotationProps
};

function onSubmit() {
// TO DO: endpoint logic is in progress on a different branch
if (selectedAnnotation && newValues?.trim()) {
dispatch(interaction.actions.editFiles({ [selectedAnnotation]: [newValues.trim()] }));
}
props.onDismiss();
}

Expand All @@ -91,7 +96,7 @@ export default function ExistingAnnotationPathway(props: ExistingAnnotationProps
{valueCount && (
<PrimaryButton
className={styles.primaryButton}
disabled={!newValues}
disabled={!newValues?.trim()}
title=""
text="REPLACE"
onClick={onSubmit}
Expand Down
6 changes: 6 additions & 0 deletions packages/core/entity/Annotation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { AnnotationValue } from "../../services/AnnotationService";
* Expected JSON structure of an annotation returned from the query service.
*/
export interface AnnotationResponse {
// Undefined when pulled from a non-AICS FMS data source
annotationId?: number;
annotationDisplayName: string;
annotationName: string;
description: string;
Expand Down Expand Up @@ -70,6 +72,10 @@ export default class Annotation {
return this.annotation.units;
}

public get id(): number | undefined {
return this.annotation.annotationId;
}

/**
* Get the annotation this instance represents from a given FmsFile. An annotation on an FmsFile
* can either be at the "top-level" of the document or it can be within it's "annotations" list.
Expand Down
30 changes: 12 additions & 18 deletions packages/core/hooks/useFileAccessContextMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,24 +136,18 @@ export default (filters?: FileFilter[], onDismiss?: () => void) => {
],
},
},
...(isQueryingAicsFms
? [
{
key: "edit",
text: "Edit metadata",
title: "Edit metadata of selected files",
disabled: !filters && fileSelection.count() === 0,
iconProps: {
iconName: "Edit",
},
onClick() {
dispatch(
interaction.actions.setVisibleModal(ModalType.EditMetadata)
);
},
},
]
: []),
{
key: "edit",
text: "Edit metadata",
title: "Edit metadata for selected files",
disabled: !filters && fileSelection.count() === 0,
iconProps: {
iconName: "Edit",
},
onClick() {
dispatch(interaction.actions.setVisibleModal(ModalType.EditMetadata));
},
},
{
key: "download",
text: "Download",
Expand Down
2 changes: 1 addition & 1 deletion packages/core/services/DatabaseService/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export default abstract class DatabaseService {
_uri: string | File
): Promise<void>;

protected abstract execute(_sql: string): Promise<void>;
public abstract execute(_sql: string): Promise<void>;

private static columnTypeToAnnotationType(columnType: string): string {
switch (columnType) {
Expand Down
20 changes: 19 additions & 1 deletion packages/core/services/FileService/DatabaseFileService/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { isEmpty, isNil, uniqueId } from "lodash";

import FileService, { GetFilesRequest, SelectionAggregationResult, Selection } from "..";
import FileService, {
GetFilesRequest,
SelectionAggregationResult,
Selection,
AnnotationNameToValuesMap,
} from "..";
import DatabaseService from "../../DatabaseService";
import DatabaseServiceNoop from "../../DatabaseService/DatabaseServiceNoop";
import FileDownloadService, { DownloadResolution, DownloadResult } from "../../FileDownloadService";
Expand Down Expand Up @@ -184,4 +189,17 @@ export default class DatabaseFileService implements FileService {
uniqueId()
);
}

public editFile(fileId: string, annotations: AnnotationNameToValuesMap): Promise<void> {
const tableName = this.dataSourceNames.sort().join(", ");
const columnAssignments = Object.entries(annotations).map(
([name, values]) => `"${name}" = '${values.join(DatabaseService.LIST_DELIMITER)}'`
);
const sql = `\
UPDATE '${tableName}' \
SET ${columnAssignments.join(", ")} \
WHERE ${DatabaseService.HIDDEN_UID_ANNOTATION} = '${fileId}'; \
`;
return this.databaseService.execute(sql);
}
}
8 changes: 4 additions & 4 deletions packages/core/services/FileService/FileServiceNoop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ export default class FileServiceNoop implements FileService {
return Promise.resolve([]);
}

public getFilesAsBuffer(): Promise<Uint8Array> {
return Promise.resolve(new Uint8Array());
}

public download(): Promise<DownloadResult> {
return Promise.resolve({ downloadRequestId: "", resolution: DownloadResolution.CANCELLED });
}

public editFile(): Promise<void> {
return Promise.resolve();
}
}
36 changes: 35 additions & 1 deletion packages/core/services/FileService/HttpFileService/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import { compact, join, uniqueId } from "lodash";

import FileService, { GetFilesRequest, SelectionAggregationResult, Selection } from "..";
import FileService, {
GetFilesRequest,
SelectionAggregationResult,
Selection,
AnnotationNameToValuesMap,
} from "..";
import FileDownloadService, { DownloadResult } from "../../FileDownloadService";
import FileDownloadServiceNoop from "../../FileDownloadService/FileDownloadServiceNoop";
import HttpServiceBase, { ConnectionConfig } from "../../HttpServiceBase";
import { FileExplorerServiceBaseUrl } from "../../../constants";
import Annotation from "../../../entity/Annotation";
import FileSelection from "../../../entity/FileSelection";
import FileSet from "../../../entity/FileSet";
import FileDetail, { FmsFile } from "../../../entity/FileDetail";
Expand All @@ -16,13 +23,20 @@ interface Config extends ConnectionConfig {
downloadService: FileDownloadService;
}

const FESBaseUrlToMMSBaseUrlMap = {
[FileExplorerServiceBaseUrl.LOCALHOST]: "https://localhost:9060",
[FileExplorerServiceBaseUrl.STAGING]: "https://stg-aics.corp.alleninstitute.org",
[FileExplorerServiceBaseUrl.PRODUCTION]: "https://aics.corp.alleninstitute.org",
};

/**
* Service responsible for fetching file related metadata.
*/
export default class HttpFileService extends HttpServiceBase implements FileService {
private static readonly ENDPOINT_VERSION = "3.0";
public static readonly BASE_FILES_URL = `file-explorer-service/${HttpFileService.ENDPOINT_VERSION}/files`;
public static readonly BASE_FILE_COUNT_URL = `${HttpFileService.BASE_FILES_URL}/count`;
public static readonly BASE_EDIT_FILES_URL = `metadata-management-service/1.0/filemetadata`;
public static readonly SELECTION_AGGREGATE_URL = `${HttpFileService.BASE_FILES_URL}/selection/aggregate`;
private static readonly CSV_ENDPOINT_VERSION = "2.0";
public static readonly BASE_CSV_DOWNLOAD_URL = `file-explorer-service/${HttpFileService.CSV_ENDPOINT_VERSION}/files/selection/manifest`;
Expand Down Expand Up @@ -127,4 +141,24 @@ export default class HttpFileService extends HttpServiceBase implements FileServ
uniqueId()
);
}

public async editFile(
fileId: string,
annotationNameToValuesMap: AnnotationNameToValuesMap,
annotationNameToAnnotationMap?: Record<string, Annotation>
): Promise<void> {
const mmsBaseUrl = FESBaseUrlToMMSBaseUrlMap[this.baseUrl as FileExplorerServiceBaseUrl];
const url = `${mmsBaseUrl}/${HttpFileService.BASE_EDIT_FILES_URL}/${fileId}`;
const annotations = Object.entries(annotationNameToValuesMap).map(([name, values]) => {
const annotationId = annotationNameToAnnotationMap?.[name].id;
if (!annotationId) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it might be better to aggregate all missing annotations and throw a single error

throw new Error(
`Unable to edit file. Failed to find annotation id for annotation ${name}`
);
}
return { annotationId, values };
});
const requestBody = JSON.stringify({ customMetadata: { annotations } });
await this.put(url, requestBody);
SeanLeRoy marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,34 @@ describe("HttpFileService", () => {
});
});

describe("editFile", () => {
const httpClient = createMockHttpClient([
{
when: () => true,
respondWith: {},
},
]);

it("fails if unable to find id of annotation", async () => {
// Arrange
const httpFileService = new HttpFileService({
baseUrl,
httpClient,
downloadService: new FileDownloadServiceNoop(),
});

// Act / Assert
try {
await httpFileService.editFile("file_id", { ["Color"]: ["red"] });
expect(false, "Expected to throw").to.be.true;
BrianWhitneyAI marked this conversation as resolved.
Show resolved Hide resolved
} catch (e) {
expect((e as Error).message).to.equal(
"Unable to edit file. Failed to find annotation id for annotation Color"
);
}
});
});

describe("getAggregateInformation", () => {
const totalFileSize = 12424114;
const totalFileCount = 7;
Expand Down
13 changes: 12 additions & 1 deletion packages/core/services/FileService/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { AnnotationValue } from "../AnnotationService";
import { DownloadResult } from "../FileDownloadService";
import Annotation from "../../entity/Annotation";
import FileDetail from "../../entity/FileDetail";
import FileSelection from "../../entity/FileSelection";
import FileSet from "../../entity/FileSet";
Expand Down Expand Up @@ -38,14 +40,23 @@ export interface Selection {
include?: string[];
}

export interface AnnotationNameToValuesMap {
[name: string]: AnnotationValue[];
}

export default interface FileService {
baseUrl?: string;
download(
annotations: string[],
selections: Selection[],
format: "csv" | "json" | "parquet"
): Promise<DownloadResult>;
getCountOfMatchingFiles(fileSet: FileSet): Promise<number>;
editFile(
fileId: string,
annotations: AnnotationNameToValuesMap,
annotationNameToAnnotationMap?: Record<string, Annotation>
): Promise<void>;
getAggregateInformation(fileSelection: FileSelection): Promise<SelectionAggregationResult>;
getCountOfMatchingFiles(fileSet: FileSet): Promise<number>;
getFiles(request: GetFilesRequest): Promise<FileDetail[]>;
}
24 changes: 24 additions & 0 deletions packages/core/services/HttpServiceBase/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,30 @@ export default class HttpServiceBase {
return new RestServiceResponse(response.data);
}

public async put<T>(url: string, body: string): Promise<RestServiceResponse<T>> {
const encodedUrl = HttpServiceBase.encodeURI(url);
const config = { headers: { "Content-Type": "application/json" } };

let response;
try {
// if this fails, bubble up exception
response = await retry.execute(() => this.httpClient.put(encodedUrl, body, config));
} catch (err) {
// Specific errors about the failure from services will be in this path
if (axios.isAxiosError(err) && err?.response?.data?.message) {
throw new Error(JSON.stringify(err.response.data.message));
}
throw err;
}

if (response.status >= 400 || response.data === undefined) {
BrianWhitneyAI marked this conversation as resolved.
Show resolved Hide resolved
// by default axios will reject if does not satisfy: status >= 200 && status < 300
BrianWhitneyAI marked this conversation as resolved.
Show resolved Hide resolved
throw new Error(`Request for ${encodedUrl} failed`);
}

return new RestServiceResponse(response.data);
}

public async patch<T>(url: string, body: string): Promise<RestServiceResponse<T>> {
const encodedUrl = HttpServiceBase.encodeURI(url);
const config = { headers: { "Content-Type": "application/json" } };
Expand Down
27 changes: 27 additions & 0 deletions packages/core/state/interaction/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { uniqueId } from "lodash";
import { ContextMenuItem, PositionReference } from "../../components/ContextMenu";
import FileFilter from "../../entity/FileFilter";
import { ModalType } from "../../components/Modal";
import { AnnotationValue } from "../../services/AnnotationService";
import { UserSelectedApplication } from "../../services/PersistentConfigService";
import FileDetail from "../../entity/FileDetail";
import { Source } from "../../entity/FileExplorerURL";
Expand Down Expand Up @@ -267,6 +268,32 @@ export function initializeApp(baseUrl: string): InitializeApp {
};
}

/**
* Edit the currently selected files with the given metadata
*/
export const EDIT_FILES = makeConstant(STATE_BRANCH_NAME, "edit-files");

export interface EditFiles {
type: string;
payload: {
annotations: { [name: string]: AnnotationValue[] };
filters?: FileFilter[];
};
}

export function editFiles(
annotations: { [name: string]: AnnotationValue[] },
filters?: FileFilter[]
): EditFiles {
return {
type: EDIT_FILES,
payload: {
annotations,
filters,
},
};
}

/**
* PROCESS AND STATUS RELATED ENUMS, INTERFACES, ETC.
*/
Expand Down
Loading
Loading