Skip to content

Commit

Permalink
Merge pull request #372 from AllenInstitute/feature/metadata-editing/…
Browse files Browse the repository at this point in the history
…create-mms-endpoint-logic

[Large] Create logic for editing metadata
  • Loading branch information
aswallace authored Jan 9, 2025
2 parents 764877e + 03fd374 commit 3e4d5e3
Show file tree
Hide file tree
Showing 16 changed files with 542 additions and 40 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
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 { AnnotationType } from "../../entity/AnnotationFormatter";
import { interaction } from "../../state";

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

Expand All @@ -21,6 +23,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 @@ -68,7 +71,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 @@ -94,7 +99,7 @@ export default function ExistingAnnotationPathway(props: ExistingAnnotationProps
{valueCount && (
<PrimaryButton
className={styles.primaryButton}
disabled={!newValues}
disabled={!newValues?.trim()}
title=""
text="REPLACE"
onClick={onSubmit}
Expand Down
8 changes: 4 additions & 4 deletions packages/core/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,10 @@ export enum FESBaseUrl {
}

export enum MMSBaseUrl {
LOCALHOST = "http://localhost:9060",
STAGING = "http://stg-aics-api",
PRODUCTION = "http://prod-aics-api",
TEST = "http://test-aics-api",
LOCALHOST = "https://localhost:9060",
STAGING = "https://stg-aics.corp.alleninstitute.org",
PRODUCTION = "https://aics.corp.alleninstitute.org",
TEST = "http://test-aics-mms-api.corp.alleninstitute.org",
}

export enum LoadBalancerBaseUrl {
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));
},
},
...(isQueryingAicsFms && !isOnWeb
? [
{
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();
}
}
28 changes: 27 additions & 1 deletion packages/core/services/FileService/HttpFileService/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
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 Annotation from "../../../entity/Annotation";
import FileSelection from "../../../entity/FileSelection";
import FileSet from "../../../entity/FileSet";
import FileDetail, { FmsFile } from "../../../entity/FileDetail";
Expand All @@ -24,6 +30,7 @@ export default class HttpFileService extends HttpServiceBase implements FileServ
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_FILE_EDIT_URL = `metadata-management-service/1.0/filemetadata`;
public static readonly BASE_FILE_CACHE_URL = `fss2/${HttpFileService.CACHE_ENDPOINT_VERSION}/file/cache`;
public static readonly SELECTION_AGGREGATE_URL = `${HttpFileService.BASE_FILES_URL}/selection/aggregate`;
private static readonly CSV_ENDPOINT_VERSION = "2.0";
Expand Down Expand Up @@ -132,6 +139,25 @@ export default class HttpFileService extends HttpServiceBase implements FileServ
);
}

public async editFile(
fileId: string,
annotationNameToValuesMap: AnnotationNameToValuesMap,
annotationNameToAnnotationMap?: Record<string, Annotation>
): Promise<void> {
const requestUrl = `${this.metadataManagementServiceBaseURl}/${HttpFileService.BASE_FILE_EDIT_URL}/${fileId}`;
const annotations = Object.entries(annotationNameToValuesMap).map(([name, values]) => {
const annotationId = annotationNameToAnnotationMap?.[name].id;
if (!annotationId) {
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(requestUrl, requestBody);
}

/**
* Cache a list of files to NAS cache (VAST) by sending their IDs to FSS.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createMockHttpClient } from "@aics/redux-utils";
import { expect } from "chai";

import HttpFileService from "..";
import { FESBaseUrl, LoadBalancerBaseUrl } from "../../../../constants";
import { FESBaseUrl, LoadBalancerBaseUrl, MMSBaseUrl } from "../../../../constants";
import FileSelection from "../../../../entity/FileSelection";
import FileSet from "../../../../entity/FileSet";
import NumericRange from "../../../../entity/NumericRange";
Expand All @@ -11,6 +11,7 @@ import FileDownloadServiceNoop from "../../../FileDownloadService/FileDownloadSe
describe("HttpFileService", () => {
const fileExplorerServiceBaseUrl = FESBaseUrl.TEST;
const loadBalancerBaseUrl = LoadBalancerBaseUrl.TEST;
const metadataManagementServiceBaseURl = MMSBaseUrl.TEST;
const fileIds = ["abc123", "def456", "ghi789", "jkl012"];
const files = fileIds.map((file_id) => ({
file_id,
Expand Down Expand Up @@ -46,6 +47,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({
metadataManagementServiceBaseURl,
httpClient,
downloadService: new FileDownloadServiceNoop(),
});

// Act / Assert
try {
await httpFileService.editFile("file_id", { ["Color"]: ["red"] });
expect(false, "Expected to throw").to.be.true;
} 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 {
fileExplorerServiceBaseUrl?: 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 @@ -252,6 +252,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) {
// by default axios will reject if does not satisfy: status >= 200 && status < 300
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 @@ -265,6 +266,32 @@ export const initializeApp = (payload: { environment: string }) => ({
payload,
});

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

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

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

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

0 comments on commit 3e4d5e3

Please sign in to comment.