Skip to content
Open
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
31 changes: 23 additions & 8 deletions lib/Core/getPath.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,37 @@
import i18next, { i18n } from "i18next";
import { applyTranslationIfExists } from "../Language/languageHelpers";
import CatalogMemberMixin from "../ModelMixins/CatalogMemberMixin";
import getAncestors from "../Models/getAncestors";
import { BaseModel } from "../Models/Definition/Model";
import getDereferencedIfExists from "./getDereferencedIfExists";
import isDefined from "./isDefined";

export default function getPath(item: BaseModel, separator?: string) {
// Pass the reactive i18n from useTranslation() in React components so the result
// updates when the user switches language. The global i18next fallback is fine for
// analytics/non-UI code where re-rendering is irrelevant.
export default function getPath(
item: BaseModel,
separator?: string,
i18nInstance: i18n = i18next
) {
const sep = isDefined(separator) ? separator : "/";
return getParentGroups(item).join(sep);
return getParentGroups(item, i18nInstance).join(sep);
}

export function getParentGroups(item: BaseModel) {
export function getParentGroups(item: BaseModel, i18nInstance: i18n = i18next) {
const dereferenced = getDereferencedIfExists(item);
return [
...getAncestors(dereferenced).map(getDereferencedIfExists),
dereferenced
].map(
(ancestor) =>
(CatalogMemberMixin.isMixedInto(ancestor) && ancestor.nameInCatalog) ||
ancestor.uniqueId
);
].map((ancestor) => {
if (CatalogMemberMixin.isMixedInto(ancestor)) {
const name = ancestor.nameInCatalog || "";
return (
(i18nInstance && applyTranslationIfExists(name, i18nInstance)) ||
name ||
ancestor.uniqueId
);
}
return ancestor.uniqueId;
});
}
29 changes: 16 additions & 13 deletions lib/ModelMixins/CatalogMemberMixin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import {
makeObservable,
override
} from "mobx";
import i18next, { i18n as i18nType } from "i18next";
import Mustache from "mustache";
import AbstractConstructor from "../Core/AbstractConstructor";
import { applyTranslationIfExists } from "../Language/languageHelpers";
import AsyncLoader from "../Core/AsyncLoader";
import isDefined from "../Core/isDefined";
import { isJsonObject, isJsonString, JsonObject } from "../Core/Json";
Expand Down Expand Up @@ -236,9 +238,8 @@ function CatalogMemberMixin<T extends AbstractConstructor<BaseType>>(Base: T) {
const descriptionRegex = /description/i;

namespace CatalogMemberMixin {
export interface Instance extends InstanceType<
ReturnType<typeof CatalogMemberMixin>
> {}
export interface Instance
extends InstanceType<ReturnType<typeof CatalogMemberMixin>> {}
export function isMixedInto(model: any): model is Instance {
return model && model.hasCatalogMemberMixin;
}
Expand All @@ -247,16 +248,18 @@ namespace CatalogMemberMixin {
export default CatalogMemberMixin;

/** Convenience function to get user readable name of a BaseModel */
export const getName = action((model: BaseModel | undefined) => {
return (
(CatalogMemberMixin.isMixedInto(model) ? model.name : undefined) ??
(hasTraits(model, CatalogMemberReferenceTraits, "name")
? model.name
: undefined) ??
model?.uniqueId ??
"Unknown model"
);
});
export const getName = action(
(model: BaseModel | undefined, i18nInstance: i18nType = i18next) => {
const name =
(CatalogMemberMixin.isMixedInto(model) ? model.name : undefined) ??
(hasTraits(model, CatalogMemberReferenceTraits, "name")
? model.name
: undefined) ??
model?.uniqueId ??
"Unknown model";
return applyTranslationIfExists(name, i18nInstance) || name;
}
);

/** Recursively apply mustache template to all nested string properties in a JSON Object */
function mustacheNestedJsonObject(obj: JsonObject, view: any) {
Expand Down
23 changes: 19 additions & 4 deletions lib/Models/Catalog/Ows/WebMapServiceCapabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,24 @@ export default class WebMapServiceCapabilities {
return Promise.resolve(loadXML(url)).then(function (capabilitiesXml) {
const json = xml2json(capabilitiesXml);
if (!capabilitiesXml || !defined(json.Capability)) {
// The server may answer with a ServiceExceptionReport instead of a
// Capabilities document (e.g. GeoServer rejecting `AcceptLanguages`
// when no internationalized content is defined). Surface its text so
// the cause is visible rather than the generic message below.
const serviceException =
json?.ServiceExceptionReport?.ServiceException ??
json?.ServiceException;
if (defined(serviceException)) {
const message =
typeof serviceException === "string"
? serviceException
: serviceException?.toString?.() ??
JSON.stringify(serviceException);
throw networkRequestError({
title: "Server refused the request",
message: `The URL ${url} returned a WMS ServiceException:\n\n${message}`
});
}
throw networkRequestError({
title: "Invalid GetCapabilities",
message:
Expand All @@ -190,10 +208,7 @@ export default class WebMapServiceCapabilities {
readonly [name: string]: CapabilitiesLayer;
};

private constructor(
readonly xml: XMLDocument,
readonly json: any
) {
private constructor(readonly xml: XMLDocument, readonly json: any) {
this.allLayers = [];
this.rootLayers = [];
this.topLevelNamedLayers = [];
Expand Down
42 changes: 32 additions & 10 deletions lib/Models/Catalog/Ows/WebMapServiceCapabilitiesStratum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,36 @@ export default class WebMapServiceCapabilitiesStratum extends LoadableStratum(
});
}

if (!isDefined(capabilities))
capabilities = await WebMapServiceCapabilities.fromUrl(
proxyCatalogItemUrl(
catalogItem,
catalogItem.getCapabilitiesUrl,
catalogItem.getCapabilitiesCacheDuration
)
);
if (!isDefined(capabilities)) {
const getCapabilitiesUrl = catalogItem.getCapabilitiesUrl;
try {
capabilities = await WebMapServiceCapabilities.fromUrl(
proxyCatalogItemUrl(
catalogItem,
getCapabilitiesUrl,
catalogItem.getCapabilitiesCacheDuration
)
);
} catch (e) {
// Some WMS servers reject the `AcceptLanguages` parameter with a
// ServiceException when no internationalized content is defined for the
// requested language(s). Retry once without it before giving up.
const urlWithoutLanguage = new URI(getCapabilitiesUrl)
.removeQuery("AcceptLanguages")
.toString();
// Param wasn't present, so there's nothing to retry - rethrow original error.
if (urlWithoutLanguage === getCapabilitiesUrl) {
throw e;
}
capabilities = await WebMapServiceCapabilities.fromUrl(
proxyCatalogItemUrl(
catalogItem,
urlWithoutLanguage,
catalogItem.getCapabilitiesCacheDuration
)
);
}
}

return new WebMapServiceCapabilitiesStratum(catalogItem, capabilities);
}
Expand Down Expand Up @@ -876,8 +898,8 @@ export default class WebMapServiceCapabilitiesStratum extends LoadableStratum(
const formatsArray = isJsonArray(formats)
? formats
: isJsonString(formats)
? [formats]
: [];
? [formats]
: [];

if (this.catalogItem.supportsGetTimeseries) {
return { format: "text/csv", type: "text" };
Expand Down
13 changes: 12 additions & 1 deletion lib/Models/Catalog/Ows/WebMapServiceCatalogItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,11 +321,16 @@ class WebMapServiceCatalogItem
this.uri.clone()
);

const lng = i18next.resolvedLanguage ?? i18next.language;
const fallbackLng = Array.isArray(i18next.options.fallbackLng)
? i18next.options.fallbackLng[0]
: (i18next.options.fallbackLng as string);
return baseUrl
.setSearch({
service: "WMS",
version: this.useWmsVersion130 ? "1.3.0" : "1.1.1",
request: "GetCapabilities"
request: "GetCapabilities",
...(lng ? { AcceptLanguages: `${lng} ${fallbackLng} *` } : {})
})
.toString();
} else {
Expand Down Expand Up @@ -379,6 +384,9 @@ class WebMapServiceCatalogItem
new URI(this.url)
);

if (i18next.language) {
baseUrl.addSearch("LANGUAGE", i18next.language);
}
return baseUrl.toString();
}

Expand All @@ -401,6 +409,9 @@ class WebMapServiceCatalogItem
if (time) {
uri.addQuery("time", time);
}
if (i18next.language) {
uri.addQuery("LANGUAGE", i18next.language);
}
return uri.toString();
}

Expand Down
12 changes: 7 additions & 5 deletions lib/ReactViews/Custom/FeedbackLinkCustomComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import CustomComponent, {
DomElement,
ProcessNodeContext
} from "./CustomComponent";
import parseCustomMarkdownToReact from "./parseCustomMarkdownToReact";
import { parseCustomMarkdownToReactWithOptions } from "./parseCustomMarkdownToReact";

function showFeedback(viewState: ViewState) {
runInAction(() => {
Expand All @@ -34,22 +34,24 @@ export const FeedbackLink = (props: {
`}
>
<Text bold isLink>
{parseCustomMarkdownToReact(
{parseCustomMarkdownToReactWithOptions(
props.feedbackMessage
? props.feedbackMessage
: i18next.t("models.raiseError.notificationFeedback")
: i18next.t("models.raiseError.notificationFeedback"),
{ inline: true }
)}
</Text>
</RawButton>
) : (
// If we only have supportEmail - show message and the email address
<>
{parseCustomMarkdownToReact(
{parseCustomMarkdownToReactWithOptions(
props.emailMessage
? `${props.emailMessage} ${props.viewState.terria.supportEmail}`
: i18next.t("models.raiseError.notificationFeedbackEmail", {
email: props.viewState.terria.supportEmail
})
}),
{ inline: true }
)}
</>
);
Expand Down
21 changes: 15 additions & 6 deletions lib/ReactViews/DataCatalog/DataCatalogGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import { observer } from "mobx-react";
import { useTranslation } from "react-i18next";
import addedByUser from "../../Core/addedByUser";
import getPath from "../../Core/getPath";
import { applyTranslationIfExists } from "../../Language/languageHelpers";
import removeUserAddedData from "../../Models/Catalog/removeUserAddedData";
import ViewState from "../../ReactViewModels/ViewState";
import Result from "../../Core/Result";
import { BaseModel } from "../../Models/Definition/Model";
import CatalogGroup from "./CatalogGroup";
import DataCatalogMember from "./DataCatalogMember";
Expand All @@ -23,7 +25,7 @@ interface GroupModel extends BaseModel {
displayGroup?: boolean;
members: any[];
memberModels: any[];
loadMembers: () => void;
loadMembers: () => Promise<Result<void>>;
nameInCatalog?: string;
url?: string;
uniqueId: string;
Expand Down Expand Up @@ -55,7 +57,7 @@ const DataCatalogGroup: React.FC<PropsType> = observer((props) => {
isTopLevel
} = props;

const { t } = useTranslation();
const { t, i18n } = useTranslation();
const [isOpenLocal, setIsOpenLocal] = useState(false);

const isOpen = useCallback(() => {
Expand Down Expand Up @@ -83,7 +85,10 @@ const DataCatalogGroup: React.FC<PropsType> = observer((props) => {

const getNameOrPrettyUrl = useCallback(() => {
// Grab a name via nameInCatalog, if it's a blank string, try and generate one from the url
const nameInCatalog = group.nameInCatalog || "";
const nameInCatalog = applyTranslationIfExists(
group.nameInCatalog || "",
i18n
);
if (nameInCatalog !== "") {
return nameInCatalog;
}
Expand All @@ -98,20 +103,24 @@ const DataCatalogGroup: React.FC<PropsType> = observer((props) => {
() => [group, isOpen()],
([currentGroup, isCurrentlyOpen]) => {
if (isCurrentlyOpen && currentGroup) {
(currentGroup as GroupModel).loadMembers();
// Surface load failures (e.g. WMS GetCapabilities ServiceException) to
// the user instead of letting them fail silently in the console.
(currentGroup as GroupModel).loadMembers().then((result) => {
result.raiseError(viewState.terria);
});
}
},
{ equals: comparer.shallow, fireImmediately: true }
);

return () => cleanupLoadMembersReaction();
}, [group, isOpen]);
}, [group, isOpen, viewState]);

return (
<CatalogGroup
text={getNameOrPrettyUrl()}
isPrivate={group.isPrivate}
title={getPath(group, " → ")}
title={getPath(group, " → ", i18n)}
topLevel={isTopLevel}
open={isOpen()}
loading={group.isLoading || group.isLoadingMembers}
Expand Down
7 changes: 4 additions & 3 deletions lib/ReactViews/DataCatalog/DataCatalogItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { MouseEvent } from "react";
import { useTranslation } from "react-i18next";
import defined from "terriajs-cesium/Source/Core/defined";
import addedByUser from "../../Core/addedByUser";
import { applyTranslationIfExists } from "../../Language/languageHelpers";
import { DataSourceAction } from "../../Core/Analytics/analyticEvents";
import getPath from "../../Core/getPath";
import CatalogFunctionMixin from "../../ModelMixins/CatalogFunctionMixin";
Expand Down Expand Up @@ -34,7 +35,7 @@ export default observer(function DataCatalogItem({
removable,
hideActionButton
}: Props) {
const { t } = useTranslation();
const { t, i18n } = useTranslation();
const STATE_TO_TITLE = {
[ButtonState.Loading]: t("catalogItem.loading"),
[ButtonState.Remove]: t("catalogItem.removeFromMap"),
Expand Down Expand Up @@ -102,9 +103,9 @@ export default observer(function DataCatalogItem({
<CatalogItem
onTextClick={setPreviewedItem}
selected={isSelected}
text={item.nameInCatalog!}
text={applyTranslationIfExists(item.nameInCatalog!, i18n)}
isPrivate={item.isPrivate}
title={getPath(item, " -> ")}
title={getPath(item, " -> ", i18n)}
btnState={btnState}
onBtnClick={onBtnClicked}
hideBtn={hideActionButton}
Expand Down
5 changes: 4 additions & 1 deletion lib/ReactViews/DataCatalog/DataCatalogReference.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { runInAction } from "mobx";
import { observer } from "mobx-react";
import { MouseEvent } from "react";
import { useTranslation } from "react-i18next";
import defined from "terriajs-cesium/Source/Core/defined";
import addedByUser from "../../Core/addedByUser";
import { DataSourceAction } from "../../Core/Analytics/analyticEvents";
Expand Down Expand Up @@ -35,6 +36,8 @@ export default observer(function DataCatalogReference({
isTopLevel,
hideActionButton
}: Props) {
const { i18n } = useTranslation();

const setPreviewedItem = () =>
viewState
.viewCatalogMember(reference)
Expand Down Expand Up @@ -68,7 +71,7 @@ export default observer(function DataCatalogReference({
? viewState.userDataPreviewedItem === reference
: viewState.previewedItem === reference;

const path = getPath(reference, " -> ");
const path = getPath(reference, " -> ", i18n);

let btnState: ButtonState;
if (reference.isLoading) {
Expand Down
Loading