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

feat(hub-common): add new utils for a unified download flow #1482

Merged
merged 31 commits into from
May 1, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
94effe5
feat(hub-common): first pass at implementing fetchDownloadFormats(), …
sonofflynn89 Apr 16, 2024
64a8b01
feat(hub-common): stub in fetchDownloadFileUrl()
sonofflynn89 Apr 16, 2024
1f0cd64
refactor(hub-common): add "/helpers/format-fetchers" subfolder to imp…
sonofflynn89 Apr 17, 2024
296393e
refactor(hub-common): add "/helpers/file-url-fetchers" subdirectory f…
sonofflynn89 Apr 17, 2024
91820f2
feat(hub-common): add first pass at fetchDownloadFileUrl helper imple…
sonofflynn89 Apr 17, 2024
187fd25
refactor(hub-common): change helper file name
sonofflynn89 Apr 17, 2024
c88e838
feat(hub-common): hub-common
sonofflynn89 Apr 17, 2024
61e657b
docs(hub-common): add todo in the export item flow
sonofflynn89 Apr 17, 2024
705faff
fix(hub-common): fix minor integration details
sonofflynn89 Apr 18, 2024
665627a
feat(hub-common): flesh out fetchDownloadFormats and fetchDownloadFil…
sonofflynn89 Apr 23, 2024
53e5089
Merge branch 'master' into f/9627-unified-service-download-logic
sonofflynn89 Apr 23, 2024
0f87100
refactor(hub-common): change "geometry" from any to __esri.geometry, …
sonofflynn89 Apr 24, 2024
33eb26f
Merge branch 'master' into f/9627-unified-service-download-logic
sonofflynn89 Apr 24, 2024
c65e532
feat(hub-common): change DownloadOperationStatus' PROCESSING to CONVE…
sonofflynn89 Apr 25, 2024
e3946e7
docs(hub-common): add documentation to the fetchDownloadFileUrl and f…
sonofflynn89 Apr 25, 2024
ada78df
feat(hub-common): add permission for displaying the new unified downl…
sonofflynn89 Apr 25, 2024
d95b6c8
refactor(hub-common): refactor metadata fetching to happen within fet…
sonofflynn89 Apr 26, 2024
773fbf7
refactor(hub-common): move private helpers into an _internal folder
sonofflynn89 Apr 26, 2024
44d8bda
refactor(hub-common): leverage dynamic imports
sonofflynn89 Apr 26, 2024
c2b03d3
feat(hub-common): expose "canUseCreateReplica" and "canUseHubDownload…
sonofflynn89 Apr 26, 2024
6632804
refactor(hub-common): leverage fetchItemEnrichments() instead of expo…
sonofflynn89 Apr 29, 2024
4724e72
Merge branch 'master' into f/9627-unified-service-download-logic
sonofflynn89 Apr 29, 2024
549ae6f
test(hub-common): fix compilation error with tests
sonofflynn89 Apr 29, 2024
9c7e7ab
feat(hub-common): add pollInterval option, update status maps, add tests
sonofflynn89 Apr 30, 2024
025d290
feat(hub-common): add additional tests and further cleanup
sonofflynn89 Apr 30, 2024
331b201
test(hub-common): add additional tests
sonofflynn89 Apr 30, 2024
99fa7c1
refactor(hub-common): refactor code and add tests to achieve 100% cod…
sonofflynn89 May 1, 2024
5222892
refactor(hub-common): refactor fetchExportImageDownloadFileUrl() to n…
sonofflynn89 May 1, 2024
11b56ca
Merge branch 'master' into f/9627-unified-service-download-logic
sonofflynn89 May 1, 2024
465b8b0
test(hub-common): add missing test assertion and doc comment
sonofflynn89 May 1, 2024
4380f20
refactor(hub-common): calculate additionalResources in computeProps u…
sonofflynn89 May 1, 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
9 changes: 9 additions & 0 deletions packages/common/src/content/_internal/computeProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
ServiceCapabilities,
} from "../hostedServiceUtils";
import { computeBaseProps } from "../../core/_internal/computeBaseProps";
import { getProp } from "../../objects";
import { IHubEditableContentEnrichments } from "../../items/_enrichments";

export function computeProps(
Expand Down Expand Up @@ -58,7 +59,15 @@ export function computeProps(
ServiceCapabilities.EXTRACT,
enrichments.server
);
const extractFormatsList: string = getProp(
enrichments,
"server.supportedExportFormats"
);
content.serverExtractFormats =
extractFormatsList && extractFormatsList.split(",");
}

content.additionalResources = enrichments.additionalResources;

return content as IHubEditableContent;
}
13 changes: 8 additions & 5 deletions packages/common/src/content/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,13 +251,16 @@ export const fetchHubContent = async (
requestOptions: IRequestOptions
): Promise<IHubEditableContent> => {
// NOTE: b/c we have to support slugs we use fetchContent() to get the item
// by telling it to not fetch any enrichments
// which we then fetch as needed after we have the item
// by telling it to not fetch any enrichments which we then fetch as needed after we have the item
// EXCEPTION: We ask it to fetch the metadata enrichment so we don't have to parse xml on the client
const options = {
...requestOptions,
enrichments: [],
enrichments: ["metadata"],
sonofflynn89 marked this conversation as resolved.
Show resolved Hide resolved
} as IFetchContentOptions;
const { item, access } = await fetchContent(identifier, options);
const { item, access, additionalResources } = await fetchContent(
sonofflynn89 marked this conversation as resolved.
Show resolved Hide resolved
identifier,
options
);

// we must normalize the underlying item type to account
// for older items (e.g. sites that are type "Web Mapping
Expand All @@ -266,7 +269,7 @@ export const fetchHubContent = async (
setProp("type", type, item);

const model = { item };
const enrichments: IHubEditableContentEnrichments = {};
const enrichments: IHubEditableContentEnrichments = { additionalResources };
if (isHostedFeatureServiceItem(item)) {
enrichments.server = await getService({
...requestOptions,
Expand Down
11 changes: 11 additions & 0 deletions packages/common/src/core/types/IHubEditableContent.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { IWithPermissions, IWithSlug } from "../traits/index";
import { IHubAdditionalResource } from "./IHubAdditionalResource";
import { IHubItemEntity, IHubItemEntityEditor } from "./IHubItemEntity";
import { IHubSchedule } from "./IHubSchedule";

Expand All @@ -19,6 +20,16 @@ export interface IHubEditableContent
* capability enabled. This is a pre-requisite for Hosted Downloads to work.
*/
serverExtractCapability?: boolean;
/**
* If the item represents a hosted feature service with "Extract enabled", shows the formats that
* can be extracted from the service via the "createReplica" operation.
*/
serverExtractFormats?: string[];
sonofflynn89 marked this conversation as resolved.
Show resolved Hide resolved
/**
* links to additional resources specified in the formal item metadata
sonofflynn89 marked this conversation as resolved.
Show resolved Hide resolved
* TODO: which items can have these? And is it only available for public items?
*/
additionalResources?: IHubAdditionalResource[];
/**
* The schedule at which the reharvest of the item will occur
*/
Expand Down
27 changes: 27 additions & 0 deletions packages/common/src/downloads/fetchDownloadFileUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import HubError from "../HubError";
import { canUseExportImageFlow } from "./helpers/canUseExportImageFlow";
import { canUseExportItemFlow } from "./helpers/canUseExportItemFlow";
import { canUseHubDownloadApi } from "./helpers/canUseHubDownloadApi";
import { fetchExportImageDownloadFileUrl } from "./helpers/file-url-fetchers/fetchExportImageDownloadFileUrl";
import { fetchExportItemDownloadFileUrl } from "./helpers/file-url-fetchers/fetchExportItemDownloadFileUrl";
import { fetchHubApiDownloadFileUrl } from "./helpers/file-url-fetchers/fetchHubApiDownloadFileUrl";
import { IFetchDownloadFileUrlOptions } from "./helpers/types";

export async function fetchDownloadFileUrl(
options: IFetchDownloadFileUrlOptions
): Promise<string> {
let fetchingFn;
if (canUseHubDownloadApi(options.entity, options.context)) {
fetchingFn = fetchHubApiDownloadFileUrl;
} else if (canUseExportItemFlow(options.entity)) {
fetchingFn = fetchExportItemDownloadFileUrl;
} else if (canUseExportImageFlow(options.entity)) {
fetchingFn = fetchExportImageDownloadFileUrl;
sonofflynn89 marked this conversation as resolved.
Show resolved Hide resolved
} else {
throw new HubError(
"fetchDownloadFileUrl",
"Downloads are not supported for this item in this environment"
);
}
return fetchingFn(options);
}
45 changes: 45 additions & 0 deletions packages/common/src/downloads/fetchDownloadFormats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { IHubAdditionalResource } from "../core/types/IHubAdditionalResource";
import { canUseExportImageFlow } from "./helpers/canUseExportImageFlow";
import { canUseExportItemFlow } from "./helpers/canUseExportItemFlow";
import { canUseHubDownloadApi } from "./helpers/canUseHubDownloadApi";
import { fetchExportItemFormats } from "./helpers/format-fetchers/fetchExportItemFormats";
import { getExportImageDownloadFormats } from "./helpers/format-fetchers/getExportImageFormats";
import { getHubDownloadApiFormats } from "./helpers/format-fetchers/getHubDownloadApiFormats";
import {
IDownloadFormat,
IFetchDownloadFormatsOptions,
IStaticDownloadFormat,
} from "./helpers/types";

export async function fetchDownloadFormats(
options: IFetchDownloadFormatsOptions
): Promise<IDownloadFormat[]> {
const { entity, context, layers } = options;
// fetch base formats for the item
let baseFormats: IDownloadFormat[] = [];
if (canUseHubDownloadApi(entity, context)) {
baseFormats = getHubDownloadApiFormats(entity);
} else if (canUseExportItemFlow(entity)) {
baseFormats = await fetchExportItemFormats(entity, context, layers);
} else if (canUseExportImageFlow(entity)) {
baseFormats = getExportImageDownloadFormats();
sonofflynn89 marked this conversation as resolved.
Show resolved Hide resolved
}

// add additional resource links as static formats
const additionalFormats = (entity.additionalResources || []).map(
toStaticFormat
);

// combine formats into single list
return [...baseFormats, ...additionalFormats];
}

function toStaticFormat(
resource: IHubAdditionalResource
): IStaticDownloadFormat {
return {
type: "static",
label: resource.name,
url: resource.url,
};
}
Original file line number Diff line number Diff line change
@@ -1,48 +1,11 @@
import { SearchQueryBuilder } from "@esri/arcgis-rest-portal";
import { ISpatialReference } from "@esri/arcgis-rest-types";
import { btoa } from "abab";
import { flattenArray } from "../util";
import { flattenArray } from "../../util";
import { PORTAL_EXPORT_TYPES } from "./types";

export const WGS84_WKID = "4326";

export const PORTAL_EXPORT_TYPES = {
csv: {
name: "CSV",
itemTypes: ["CSV", "CSV Collection"],
supportsProjection: true,
},
kml: {
name: "KML",
itemTypes: ["KML", "KML Collection"],
supportsProjection: false,
},
shapefile: {
name: "Shapefile",
itemTypes: ["Shapefile"],
supportsProjection: true,
},
fileGeodatabase: {
name: "File Geodatabase",
itemTypes: ["File Geodatabase"],
supportsProjection: true,
},
geojson: {
name: "GeoJson",
itemTypes: ["GeoJson"],
supportsProjection: false,
},
excel: {
name: "Excel",
itemTypes: ["Microsoft Excel"],
supportsProjection: true,
},
featureCollection: {
name: "Feature Collection",
itemTypes: ["Feature Collection"],
supportsProjection: true,
},
};

interface IExistingExportsPortalQueryOptions {
layerId?: number | string;
onlyTypes?: string[];
Expand Down
10 changes: 10 additions & 0 deletions packages/common/src/downloads/helpers/canCreateExportItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { IArcGISContext } from "../../ArcGISContext";
import { IHubEditableContent } from "../../core/types/IHubEditableContent";

export function canCreateExportItem(
_entity: IHubEditableContent,
_context: IArcGISContext
) {
// TODO: port over logic from ember's download-service
return true;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { IHubEditableContent } from "../../core/types/IHubEditableContent";

export function canUseExportImageFlow(entity: IHubEditableContent): boolean {
return entity.type === "Image Service";
}
6 changes: 6 additions & 0 deletions packages/common/src/downloads/helpers/canUseExportItemFlow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { isHostedFeatureServiceEntity } from "../../content";
import { IHubEditableContent } from "../../core/types/IHubEditableContent";

export function canUseExportItemFlow(entity: IHubEditableContent): boolean {
return isHostedFeatureServiceEntity(entity);
}
22 changes: 22 additions & 0 deletions packages/common/src/downloads/helpers/canUseHubDownloadApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { IArcGISContext } from "../../ArcGISContext";
import { IHubEditableContent } from "../../core/types/IHubEditableContent";
import { isHostedFeatureServiceEntity } from "../../content/hostedServiceUtils";
import { isMapOrFeatureServiceEntity } from "./isMapOrFeatureServiceEntity";

export function canUseHubDownloadApi(
entity: IHubEditableContent,
context: IArcGISContext
): boolean {
// TODO: use permission instead.
sonofflynn89 marked this conversation as resolved.
Show resolved Hide resolved
const isPublicMapOrFeatureService =
isMapOrFeatureServiceEntity(entity) && entity.access === "public";
const isPrivateHostedFeatureServiceWithExtractEnabled =
isHostedFeatureServiceEntity(entity) &&
entity.serverExtractCapability &&
entity.access !== "public";
return (
!context.isPortal &&
(isPublicMapOrFeatureService ||
isPrivateHostedFeatureServiceWithExtractEnabled)
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { request } from "@esri/arcgis-rest-request";
import {
DownloadOperationStatus,
IFetchDownloadFileUrlOptions,
} from "../types";

export async function fetchExportImageDownloadFileUrl(
options: IFetchDownloadFileUrlOptions
): Promise<string> {
const { entity, format, context, geometry, progressCallback } = options;
progressCallback && progressCallback(DownloadOperationStatus.PENDING);

// TODO: validate layers, geometry, where, etc. I don't think all of them are applicable in every permutation

const requestOptions = { ...context.requestOptions };
requestOptions.httpMethod = "GET";
requestOptions.params = {
f: "image",
format,
mosaicRule:
'{"ascending":true,"mosaicMethod":"esriMosaicNorthwest","mosaicOperation":"MT_FIRST"}',
};

if (geometry) {
const { xmin, xmax, ymin, ymax } = geometry;
const { wkid } = geometry.spatialReference;
requestOptions.params.bbox = `${xmin},${ymin},${xmax},${ymax}`;
requestOptions.params.bboxSR = `${wkid}`;
requestOptions.params.imageSR = `${wkid}`;
}
// Note: validate where "extent" and "layer" are coming from in the old ember code,
// check if they are still applicable here
else {
const coords = entity.extent;
requestOptions.params.bbox = `${coords[0][0]},${coords[0][1]},${coords[1][0]},${coords[1][1]}`;
requestOptions.params.bboxSR = "4326";
requestOptions.params.imageSR = "4326";
}

// const { maxImageHeight, maxImageWidth } = this.args.model.layer || {};
// if (maxImageWidth && maxImageHeight) {
// requestOptions.params.size = `${maxImageWidth},${maxImageHeight}`;
// }

const blob = await request(`${entity.url}/exportImage`, requestOptions);
const url = URL.createObjectURL(blob);
progressCallback && progressCallback(DownloadOperationStatus.COMPLETED);
return url;
}
Loading
Loading