Skip to content
Draft
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
7 changes: 5 additions & 2 deletions i18n/en.pot
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ msgstr ""
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"POT-Creation-Date: 2025-09-26T11:41:24.766Z\n"
"PO-Revision-Date: 2025-09-26T11:41:24.766Z\n"
"POT-Creation-Date: 2025-10-01T11:23:55.690Z\n"
"PO-Revision-Date: 2025-10-01T11:23:55.690Z\n"

msgid ""
"THIS NEW RELEASE INCLUDES SHARING SETTINGS PER INSTANCES. FOR THIS VERSION "
Expand Down Expand Up @@ -1256,6 +1256,9 @@ msgstr ""
msgid "with stored user"
msgstr ""

msgid "Loading instances..."
msgstr ""

msgid "Destination"
msgstr ""

Expand Down
5 changes: 4 additions & 1 deletion i18n/es.po
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
msgid ""
msgstr ""
"Project-Id-Version: i18next-conv\n"
"POT-Creation-Date: 2025-08-19T15:09:40.837Z\n"
"POT-Creation-Date: 2025-10-01T11:23:55.690Z\n"
"PO-Revision-Date: 2020-07-10T06:53:30.625Z\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
Expand Down Expand Up @@ -1259,6 +1259,9 @@ msgstr ""
msgid "with stored user"
msgstr ""

msgid "Loading instances..."
msgstr ""

msgid "Destination"
msgstr ""

Expand Down
5 changes: 4 additions & 1 deletion i18n/fr.po
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
msgid ""
msgstr ""
"Project-Id-Version: i18next-conv\n"
"POT-Creation-Date: 2025-08-19T15:09:40.837Z\n"
"POT-Creation-Date: 2025-10-01T11:23:55.690Z\n"
"PO-Revision-Date: 2020-07-10T06:53:30.625Z\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
Expand Down Expand Up @@ -1259,6 +1259,9 @@ msgstr ""
msgid "with stored user"
msgstr ""

msgid "Loading instances..."
msgstr ""

msgid "Destination"
msgstr ""

Expand Down
5 changes: 4 additions & 1 deletion i18n/pt.po
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
msgid ""
msgstr ""
"Project-Id-Version: i18next-conv\n"
"POT-Creation-Date: 2025-08-19T15:09:40.837Z\n"
"POT-Creation-Date: 2025-10-01T11:23:55.690Z\n"
"PO-Revision-Date: 2020-07-10T06:53:30.625Z\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
Expand Down Expand Up @@ -1259,6 +1259,9 @@ msgstr ""
msgid "with stored user"
msgstr ""

msgid "Loading instances..."
msgstr ""

msgid "Destination"
msgstr ""

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ describe("MetadataPayloadBuilder", () => {
url: "http://localhost:8080",
});

describe.only("executing build method for a dashboard and a usergroup - dashboard referencing the usergroup", () => {
describe("executing build method for a dashboard and a usergroup - dashboard referencing the usergroup", () => {
it("should return expected payload when option include objects and references of sharing settings, users and organisation units is selected", async () => {
// The builder includes metadataTypes = userGroups and dashboards, metadataIds = one dashboard and one usergroup.
// The dashboard references the usergroup
Expand Down
23 changes: 21 additions & 2 deletions src/presentation/react/core/components/dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FormControl, InputLabel, MenuItem, MuiThemeProvider, Select } from "@material-ui/core";
import { CircularProgress, FormControl, InputLabel, MenuItem, MuiThemeProvider, Select } from "@material-ui/core";
import { createTheme } from "@material-ui/core/styles";
import _ from "lodash";
import React from "react";
Expand All @@ -23,6 +23,7 @@ interface DropdownProps<T extends string = string> {
view?: DropdownViewOption;
disabled?: boolean;
style?: React.CSSProperties;
loading?: boolean;
}

const getTheme = (view: DropdownViewOption) => {
Expand Down Expand Up @@ -83,6 +84,7 @@ export function Dropdown<T extends string = string>({
emptyLabel,
view = "filter",
disabled = false,
loading = false,
}: DropdownProps<T>) {
const inlineStyles = { minWidth: 120, paddingLeft: 25, paddingRight: 25 };
const styles = view === "inline" ? inlineStyles : {};
Expand All @@ -107,7 +109,8 @@ export function Dropdown<T extends string = string>({
},
}}
style={styles}
disabled={disabled}
disabled={disabled || loading}
IconComponent={loading ? () => <LoadingIcon /> : undefined}
>
{!hideEmpty && <MenuItem value={""}>{emptyLabel ?? i18n.t("<No value>")}</MenuItem>}
{items.map(element => (
Expand All @@ -121,4 +124,20 @@ export function Dropdown<T extends string = string>({
);
}

const LoadingIcon = () => (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
position: "absolute",
right: 0,
width: 24,
height: 24,
}}
>
<CircularProgress size={16} />
</div>
);

export default Dropdown;
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { Maybe } from "../../../../../types/utils";
import Dropdown, { DropdownViewOption } from "../dropdown/Dropdown";
import { useAppContext } from "../../contexts/AppContext";
import { Store } from "../../../../../domain/stores/entities/Store";
import { User } from "../../../../../domain/user/entities/User";

export type InstanceSelectionOption = "local" | "remote" | "store";

Expand All @@ -33,11 +32,11 @@ export const InstanceSelectionDropdown: React.FC<InstanceSelectionDropdownProps>
title = i18n.t("Instances"),
refreshKey,
}) => {
const { compositionRoot } = useAppContext();
const { compositionRoot, currentUser } = useAppContext();

const [instances, setInstances] = useState<Instance[]>([]);
const [stores, setStores] = useState<Store[]>([]);
const [user, setUser] = useState<User>();
const [loading, setLoading] = useState(false);

const updateSelectedInstance = useCallback(
(id: string) => {
Expand Down Expand Up @@ -68,21 +67,25 @@ export const InstanceSelectionDropdown: React.FC<InstanceSelectionDropdownProps>
}, [showInstances, instances, stores]);

useEffect(() => {
compositionRoot.user.current().then(setUser);
}, [compositionRoot.user]);

useEffect(() => {
compositionRoot.instances.list().then(instances => {
const instancesWithPermisions = user
? instances.filter(instance => instance.hasPermissions("read", user))
: [];
setInstances(instancesWithPermisions);
});
setLoading(true);
compositionRoot.instances
.list()
.then(instances => {
const instancesWithPermisions = currentUser
? instances.filter(instance => instance.hasPermissions("read", currentUser))
: [];
setInstances(instancesWithPermisions);
})
.finally(() => setLoading(false));

if (showInstances.store) {
compositionRoot.store.list().then(setStores);
setLoading(true);
compositionRoot.store
.list()
.then(setStores)
.finally(() => setLoading(false));
}
}, [compositionRoot, showInstances, refreshKey, user]);
}, [compositionRoot, showInstances, refreshKey, currentUser]);

useEffect(() => {
// Auto-select first instance
Expand All @@ -100,6 +103,7 @@ export const InstanceSelectionDropdown: React.FC<InstanceSelectionDropdownProps>
label={title}
hideEmpty={true}
view={view}
loading={loading}
/>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { MultiSelector } from "@eyeseetea/d2-ui-components";
import { makeStyles, Typography } from "@material-ui/core";
import { CircularProgress, makeStyles, Typography } from "@material-ui/core";
import React, { useEffect, useState } from "react";
import { Instance } from "../../../../../../domain/instance/entities/Instance";
import { User } from "../../../../../../domain/user/entities/User";
Expand All @@ -25,12 +25,12 @@ export const buildInstanceOptions = (instances: Instance[], currentUser: User) =
};

const InstanceSelectionStep: React.FC<SyncWizardStepProps> = ({ syncRule, onChange }) => {
const { d2, compositionRoot } = useAppContext();
const { d2, compositionRoot, currentUser } = useAppContext();
const classes = useStyles();

const [selectedOptions, setSelectedOptions] = useState<string[]>(syncRule.targetInstances);
const [targetInstances, setTargetInstances] = useState<Instance[]>([]);
const [instanceOptions, setInstanceOptions] = useState<{ value: string; text: string }[]>([]);
const [loading, setLoading] = useState(false);

const includeCurrentUrlAndTypeIsEvents = (selectedinstanceIds: string[]) => {
return (
Expand All @@ -57,22 +57,34 @@ const InstanceSelectionStep: React.FC<SyncWizardStepProps> = ({ syncRule, onChan
};

useEffect(() => {
compositionRoot.instances.list().then(instances => {
setTargetInstances(instances);
compositionRoot.user.current().then(user => setInstanceOptions(buildInstanceOptions(instances, user)));
});
setLoading(true);
compositionRoot.instances
.list()
.then(instances => {
setTargetInstances(instances);
})
.finally(() => setLoading(false));
}, [compositionRoot]);

const instanceOptions = React.useMemo(
() => buildInstanceOptions(targetInstances, currentUser),
[targetInstances, currentUser]
);

return (
<React.Fragment>
{syncRule.originInstance === "LOCAL" ? (
<MultiSelector
d2={d2}
height={300}
onChange={changeInstances}
options={instanceOptions}
selected={selectedOptions}
/>
<div className={classes.multiSelectorContainer}>
<MultiSelector
d2={d2}
height={300}
onChange={changeInstances}
options={instanceOptions}
selected={selectedOptions}
classes={{ wrapper: loading ? classes.multiSelectorWrapperLoading : "", searchField: "" }}
/>
{loading && <LoadingInstances />}
</div>
) : (
<Typography className={classes.advancedOptionsTitle} variant="subtitle1" gutterBottom>
{i18n.t("Destination")}: {i18n.t("This instance")}
Expand All @@ -88,10 +100,38 @@ const InstanceSelectionStep: React.FC<SyncWizardStepProps> = ({ syncRule, onChan
);
};

function LoadingInstances() {
const classes = useStyles();
return (
<div className={classes.loadingIndicatorContainer}>
<div className={classes.loadingMessage}>
<CircularProgress size={16} />
<Typography>{i18n.t("Loading instances...")}</Typography>
</div>
</div>
);
}

const useStyles = makeStyles({
advancedOptionsTitle: {
fontWeight: 500,
},
multiSelectorContainer: {
position: "relative",
},
multiSelectorWrapperLoading: {
visibility: "hidden",
},
loadingIndicatorContainer: {
position: "absolute",
inset: 0,
padding: 16,
},
loadingMessage: {
display: "flex",
alignItems: "center",
gap: 8,
},
});

export default InstanceSelectionStep;
2 changes: 2 additions & 0 deletions src/presentation/react/core/contexts/AppContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import React, { useContext } from "react";
import { CompositionRoot } from "../../../CompositionRoot";
import { D2Api } from "../../../../types/d2-api";
import { NewCompositionRoot } from "../../../NewCompositionRoot";
import { User } from "../../../../domain/user/entities/User";

export interface AppContextState {
api: D2Api;
d2: object;
compositionRoot: CompositionRoot;
newCompositionRoot: NewCompositionRoot;
currentUser: User;
}

export const AppContext = React.createContext<AppContextState | null>(null);
Expand Down
2 changes: 1 addition & 1 deletion src/presentation/webapp/WebApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ const App = () => {
if (!currentUser) throw new Error("User not logged in");

const newCompositionRoot = getWebappCompositionRoot(instance);
setAppContext({ d2: d2, api, compositionRoot, newCompositionRoot });
setAppContext({ d2: d2, api, compositionRoot, newCompositionRoot, currentUser });

Object.assign(window, { api });
setUsername(currentUser.username);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { makeStyles } from "@material-ui/core";
import { makeStyles, Theme } from "@material-ui/core";
import Typography from "@material-ui/core/Typography";
import ArrowRightIcon from "@material-ui/icons/ArrowRightAlt";
import React, { useCallback } from "react";
Expand Down Expand Up @@ -66,24 +66,26 @@ const InstancesSelectors: React.FC<InstancesSelectorsProps> = ({
const showAllInstances = { local: true, remote: true };
const showOnlyLocalInstances = { local: true, remote: false };

const useStyles = makeStyles({
const useStyles = makeStyles((theme: Theme) => ({
icon: {
padding: "0px 25px",
marginBottom: 5,
verticalAlign: "middle",
},
instances: {
display: "flex",
alignItems: "center",
float: "right",
padding: "0px 10px",
border: "1px solid",
padding: `5px 10px 0px 10px`,
backgroundColor: theme.palette.background.default,
boxShadow: theme.shadows[1],
},
label: {
display: "inline-block",
fontWeight: 400,
fontSize: "1rem",
marginTop: "4px",
verticalAlign: "top",
fontWeight: 500,
fontSize: "0.95rem",
marginBottom: 7,
},
});
}));

export default React.memo(InstancesSelectors);
Loading