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

feature/copy-to-local-cache-request #322

Merged
Merged
Show file tree
Hide file tree
Changes from 7 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
17 changes: 13 additions & 4 deletions packages/core/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import GlobalActionButtonRow from "./components/GlobalActionButtonRow";
import StatusMessage from "./components/StatusMessage";
import TutorialTooltip from "./components/TutorialTooltip";
import QuerySidebar from "./components/QuerySidebar";
import { FileExplorerServiceBaseUrl } from "./constants";
import { AicsLoadBalancerBaseUrl, FileExplorerServiceBaseUrl } from "./constants";
import { interaction, selection } from "./state";
import useLayoutMeasurements from "./hooks/useLayoutMeasurements";

Expand All @@ -39,11 +39,15 @@ interface AppProps {
// Localhost: "https://localhost:9081"
// Stage: "http://stg-aics-api.corp.alleninstitute.org"
// From the web (behind load balancer): "/"
aicsLoadBalancerBaseUrl?: string;
fileExplorerServiceBaseUrl?: string;
}

export default function App(props: AppProps) {
const { fileExplorerServiceBaseUrl = FileExplorerServiceBaseUrl.PRODUCTION } = props;
const {
aicsLoadBalancerBaseUrl = AicsLoadBalancerBaseUrl.PRODUCTION,
fileExplorerServiceBaseUrl = FileExplorerServiceBaseUrl.PRODUCTION,
} = props;

const dispatch = useDispatch();
const hasQuerySelected = useSelector(selection.selectors.hasQuerySelected);
Expand Down Expand Up @@ -80,8 +84,13 @@ export default function App(props: AppProps) {

// Set data source base urls
React.useEffect(() => {
dispatch(interaction.actions.initializeApp(fileExplorerServiceBaseUrl));
}, [dispatch, fileExplorerServiceBaseUrl]);
dispatch(
interaction.actions.initializeApp({
aicsLoadBalancerBaseUrl,
fileExplorerServiceBaseUrl,
})
);
}, [dispatch, aicsLoadBalancerBaseUrl, fileExplorerServiceBaseUrl]);

// Respond to screen size changes
React.useEffect(() => {
Expand Down
6 changes: 6 additions & 0 deletions packages/core/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ export enum FileExplorerServiceBaseUrl {
PRODUCTION = "https://production.int.allencell.org",
}

export enum AicsLoadBalancerBaseUrl {
LOCALHOST = "http://localhost:8080",
STAGING = "http://stg-aics.corp.alleninstitute.org",
PRODUCTION = "http://aics.corp.alleninstitute.org",
}
BrianWhitneyAI marked this conversation as resolved.
Show resolved Hide resolved

export const TOP_LEVEL_FILE_ANNOTATIONS = [
new Annotation({
annotationDisplayName: "File ID",
Expand Down
13 changes: 2 additions & 11 deletions packages/core/entity/FileDetail/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import AnnotationName from "../Annotation/AnnotationName";
import { FileExplorerServiceBaseUrl } from "../../constants";
import { FmsFileAnnotation } from "../../services/FileService";
import { renderZarrThumbnailURL } from "./RenderZarrThumbnailURL";

Expand Down Expand Up @@ -194,21 +193,13 @@ export default class FileDetail {
return this.thumbnail;
}

public getLinkToPlateUI(baseURL: string): string | undefined {
public getLinkToPlateUI(labkeyHost: string): string | undefined {
// Grabbing plate barcode
const platebarcode = this.getFirstAnnotationValue(AnnotationName.PLATE_BARCODE);

if (!platebarcode) {
return undefined;
}

let labkeyHost = "localhost:9081";
if (baseURL === FileExplorerServiceBaseUrl.PRODUCTION) {
labkeyHost = "aics.corp.alleninstitute.org";
} else if (baseURL === FileExplorerServiceBaseUrl.STAGING) {
labkeyHost = "stg-aics.corp.alleninstitute.org";
}
return `http://${labkeyHost}/labkey/aics_microscopy/AICS/editPlate.view?Barcode=${platebarcode}`;
return `${labkeyHost}/labkey/aics_microscopy/AICS/editPlate.view?Barcode=${platebarcode}`;
}

public getAnnotationNameToLinkMap(): { [annotationName: string]: string } {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/hooks/useFileAccessContextMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export default (filters?: FileFilter[], onDismiss?: () => void) => {
],
},
},
...(isQueryingAicsFms
...(isQueryingAicsFms && !isOnWeb
? [
{
key: "move-to-cache",
Expand Down
6 changes: 2 additions & 4 deletions packages/core/hooks/useOpenWithMenuItems/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -177,10 +177,9 @@ export default (fileDetails?: FileDetail, filters?: FileFilter[]): IContextualMe
const annotationNameToAnnotationMap = useSelector(
metadata.selectors.getAnnotationNameToAnnotationMap
);
const fileExplorerServiceBaseUrl = useSelector(
interaction.selectors.getFileExplorerServiceBaseUrl
);
const aicsLoadBalancerBaseUrl = useSelector(interaction.selectors.getAicsLoadBalancerBaseUrl);

const plateLink = fileDetails?.getLinkToPlateUI(aicsLoadBalancerBaseUrl);
const annotationNameToLinkMap = React.useMemo(
() =>
fileDetails?.annotations
Expand Down Expand Up @@ -249,7 +248,6 @@ export default (fileDetails?: FileDetail, filters?: FileFilter[]): IContextualMe
.filter((app) => supportedApps.every((item) => item.key !== app.key))
.sort((a, b) => (a.text || "").localeCompare(b.text || ""));

const plateLink = fileDetails?.getLinkToPlateUI(fileExplorerServiceBaseUrl);
if (plateLink && isAicsEmployee) {
supportedApps.push({
key: "open-plate-ui",
Expand Down
27 changes: 27 additions & 0 deletions packages/core/services/FileService/HttpFileService/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ interface Config extends ConnectionConfig {
* Service responsible for fetching file related metadata.
*/
export default class HttpFileService extends HttpServiceBase implements FileService {
private static readonly CACHE_ENDPOINT_VERSION = "v3.0";
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_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";
public static readonly BASE_CSV_DOWNLOAD_URL = `file-explorer-service/${HttpFileService.CSV_ENDPOINT_VERSION}/files/selection/manifest`;
Expand Down Expand Up @@ -127,4 +129,29 @@ export default class HttpFileService extends HttpServiceBase implements FileServ
uniqueId()
);
}

/**
* Cache a list of files to NAS cache (VAST) by sending their IDs to FSS.
*/
public async cacheFiles(
fileIds: string[],
username?: string
): Promise<{ cacheFileStatuses: { [fileId: string]: string } }> {
const requestUrl = `${this.aicsLoadBalancerBaseUrl}/${HttpFileService.BASE_FILE_CACHE_URL}${this.pathSuffix}`;
const requestBody = JSON.stringify({ fileIds });
const headers = {
"Content-Type": "application/json",
"X-User-Id": username || "anonymous",
};
console.log(headers);
BrianWhitneyAI marked this conversation as resolved.
Show resolved Hide resolved
try {
const cacheStatuses = await this.rawPut<{
cacheFileStatuses: { [fileId: string]: string };
}>(requestUrl, requestBody, headers);
return cacheStatuses;
} catch (error) {
console.error("Failed to cache files:", error);
throw new Error("Unable to complete the caching request.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import FileDownloadServiceNoop from "../../../FileDownloadService/FileDownloadSe

describe("HttpFileService", () => {
const baseUrl = "test";
const aicsLoadBalancerBaseUrlMock = "http://loadbalancer-test.aics.corp.alleninstitute.org";
const fileIds = ["abc123", "def456", "ghi789", "jkl012"];
const files = fileIds.map((file_id) => ({
file_id,
Expand All @@ -28,7 +29,7 @@ describe("HttpFileService", () => {

it("issues request for files that match given parameters", async () => {
const httpFileService = new HttpFileService({
baseUrl,
baseUrl: baseUrl,
httpClient,
downloadService: new FileDownloadServiceNoop(),
});
Expand Down Expand Up @@ -78,25 +79,39 @@ describe("HttpFileService", () => {
});
});

describe("getCountOfMatchingFiles", () => {
describe("cacheFiles", () => {
const httpClient = createMockHttpClient({
when: `${baseUrl}/${HttpFileService.BASE_FILE_COUNT_URL}`,
when: `${aicsLoadBalancerBaseUrlMock}/${HttpFileService.BASE_FILE_CACHE_URL}`,
respondWith: {
data: {
data: [2],
cacheFileStatuses: {
abc123: "DOWNLOAD_COMPLETE",
def456: "ERROR",
},
},
},
});

it("issues request for count of files matching given parameters", async () => {
it("sends file IDs to be cached and returns their statuses", async () => {
// Arrange
const fileService = new HttpFileService({
baseUrl,
aicsLoadBalancerBaseUrl: aicsLoadBalancerBaseUrlMock,
httpClient,
downloadService: new FileDownloadServiceNoop(),
});
const fileSet = new FileSet();
const count = await fileService.getCountOfMatchingFiles(fileSet);
expect(count).to.equal(2);
const fileIds = ["abc123", "def456"];
const username = "test.user";

// Act
const response = await fileService.cacheFiles(fileIds, username);

// Assert
expect(response).to.deep.equal({
cacheFileStatuses: {
abc123: "DOWNLOAD_COMPLETE",
def456: "ERROR",
},
});
});
});
});
48 changes: 47 additions & 1 deletion packages/core/services/HttpServiceBase/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import axios, { AxiosInstance } from "axios";
import { Policy } from "cockatiel";
import LRUCache from "lru-cache";

import { FileExplorerServiceBaseUrl } from "../../constants";
import { AicsLoadBalancerBaseUrl, FileExplorerServiceBaseUrl } from "../../constants";
import RestServiceResponse from "../../entity/RestServiceResponse";

export interface ConnectionConfig {
aicsLoadBalancerBaseUrl?: string | keyof typeof AicsLoadBalancerBaseUrl;
applicationVersion?: string;
baseUrl?: string | keyof typeof FileExplorerServiceBaseUrl;
httpClient?: AxiosInstance;
Expand All @@ -14,6 +15,7 @@ export interface ConnectionConfig {
}

export const DEFAULT_CONNECTION_CONFIG = {
aicsLoadBalancerBaseUrl: AicsLoadBalancerBaseUrl.PRODUCTION,
baseUrl: FileExplorerServiceBaseUrl.PRODUCTION,
httpClient: axios.create(),
};
Expand Down Expand Up @@ -97,15 +99,22 @@ export default class HttpServiceBase {
.join("");
}

public aicsLoadBalancerBaseUrl: string | keyof typeof AicsLoadBalancerBaseUrl =
DEFAULT_CONNECTION_CONFIG.aicsLoadBalancerBaseUrl;
public baseUrl: string | keyof typeof FileExplorerServiceBaseUrl =
DEFAULT_CONNECTION_CONFIG.baseUrl;

protected httpClient = DEFAULT_CONNECTION_CONFIG.httpClient;
private applicationVersion = "NOT SET";
private userName?: string;
protected readonly pathSuffix: string = "";
private readonly urlToResponseDataCache = new LRUCache<string, any>({ max: MAX_CACHE_SIZE });

constructor(config: ConnectionConfig = {}) {
if (config.aicsLoadBalancerBaseUrl) {
this.setAicsLoadBalancerBaseUrl(config.aicsLoadBalancerBaseUrl);
}

if (config.applicationVersion) {
this.setApplicationVersion(config.applicationVersion);
}
Expand Down Expand Up @@ -186,6 +195,32 @@ export default class HttpServiceBase {
return response.data;
}

public async rawPut<T>(
url: string,
body: string,
headers: { [key: string]: string } = {}
): Promise<T> {
const encodedUrl = HttpServiceBase.encodeURI(url);
const config = { headers: { ...headers } };

let response;
try {
// Retry policy wrapped around axios PUT
response = await retry.execute(() => this.httpClient.put(encodedUrl, body, config));
} catch (err) {
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) {
throw new Error(`Request for ${encodedUrl} failed`);
}

return response.data;
}

public async post<T>(url: string, body: string): Promise<RestServiceResponse<T>> {
const encodedUrl = HttpServiceBase.encodeURI(url);
const config = { headers: { "Content-Type": "application/json" } };
Expand Down Expand Up @@ -234,6 +269,17 @@ export default class HttpServiceBase {
return new RestServiceResponse(response.data);
}

public setAicsLoadBalancerBaseUrl(
aicsLoadBalancerBaseUrl: string | keyof typeof AicsLoadBalancerBaseUrl
) {
if (this.aicsLoadBalancerBaseUrl !== aicsLoadBalancerBaseUrl) {
// bust cache when base url changes
this.urlToResponseDataCache.reset();
}

this.aicsLoadBalancerBaseUrl = aicsLoadBalancerBaseUrl;
}

public setApplicationVersion(applicationVersion: string) {
this.applicationVersion = applicationVersion;
this.setHeaders();
Expand Down
13 changes: 7 additions & 6 deletions packages/core/state/interaction/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,12 +260,13 @@ export interface InitializeApp {
payload: string;
}

export function initializeApp(baseUrl: string): InitializeApp {
return {
type: INITIALIZE_APP,
payload: baseUrl,
};
}
export const initializeApp = (payload: {
aicsLoadBalancerBaseUrl: string;
fileExplorerServiceBaseUrl: string;
}) => ({
type: INITIALIZE_APP,
payload,
});

/**
* PROCESS AND STATUS RELATED ENUMS, INTERFACES, ETC.
Expand Down
20 changes: 15 additions & 5 deletions packages/core/state/interaction/logics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -582,12 +582,22 @@ const setIsSmallScreen = createLogic({
* Logs details of files that are being moved.
*/
const moveFilesLogic = createLogic({
type: MOVE_FILES,
process(deps, dispatch, done) {
const action = deps.action as MoveFilesAction;
console.log(`Moving files:`, action.payload.fileDetails);
done();
async process({ action, getState }: ReduxLogicDeps, _dispatch, done) {
try {
const httpFileService = interactionSelectors.getHttpFileService(getState());
const username = interactionSelectors.getUserName(getState());
BrianWhitneyAI marked this conversation as resolved.
Show resolved Hide resolved
const fileIds = (action as MoveFilesAction).payload.fileDetails.map((file) => file.id);
const cacheStatuses = await httpFileService.cacheFiles(fileIds, username);

// TODO: What to do with the status
BrianWhitneyAI marked this conversation as resolved.
Show resolved Hide resolved
console.log("Cache statuses:", cacheStatuses);
} catch (err) {
console.error(`Error encountered while moving files: ${err}`);
BrianWhitneyAI marked this conversation as resolved.
Show resolved Hide resolved
} finally {
done();
}
},
type: MOVE_FILES,
});

export default [
Expand Down
5 changes: 4 additions & 1 deletion packages/core/state/interaction/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import DatabaseServiceNoop from "../../services/DatabaseService/DatabaseServiceN
import PublicDataset from "../../../web/src/entity/PublicDataset";

export interface InteractionStateBranch {
aicsLoadBalancerBaseUrl: string;
applicationVersion?: string;
contextMenuIsVisible: boolean;
contextMenuItems: ContextMenuItem[];
Expand All @@ -67,6 +68,7 @@ export interface InteractionStateBranch {
}

export const initialState: InteractionStateBranch = {
aicsLoadBalancerBaseUrl: DEFAULT_CONNECTION_CONFIG.aicsLoadBalancerBaseUrl,
contextMenuIsVisible: false,
contextMenuItems: [],
// Passed to `ContextualMenu` as `target`. From the "@fluentui/react" docs:
Expand Down Expand Up @@ -166,7 +168,8 @@ export default makeReducer<InteractionStateBranch>(
}),
[INITIALIZE_APP]: (state, action) => ({
...state,
fileExplorerServiceBaseUrl: action.payload,
aicsLoadBalancerBaseUrl: action.payload.aicsLoadBalancerBaseUrl,
fileExplorerServiceBaseUrl: action.payload.fileExplorerServiceBaseUrl,
}),
[SET_VISIBLE_MODAL]: (state, action) => ({
...state,
Expand Down
Loading
Loading