Skip to content

Commit

Permalink
Merge pull request #606 from ianmcorvidae/user-instant-launches
Browse files Browse the repository at this point in the history
Instant launch embed code & user-created instant launches
  • Loading branch information
ianmcorvidae authored Nov 12, 2024
2 parents b909462 + e2f10af commit 6e9cb1b
Show file tree
Hide file tree
Showing 11 changed files with 220 additions and 27 deletions.
6 changes: 6 additions & 0 deletions public/static/locales/en/apps.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,10 @@
"edit": "Edit",
"editApp": "Edit App",
"editWorkflow": "Edit Workflow",
"embedInstantLaunchNotUsed": "This embed code will send users to the app launch page. If you would prefer it launch the app immediately, turn on the \"Use Instant Launch\" switch.",
"embedInstantLaunchUsed": "This embed code will cause users to launch the app immediately. If you would prefer they be sent to the app launch page, turn off the \"Use Instant Launch\" switch.",
"embedLbl": "Embed",
"embedNoInstantLaunch": "This embed code will send users to the app launch page. If you would prefer it launch the app immediately, click the \"Create Instant Launch\" button.",
"emptyValue": "Empty Value",
"favMutationError": "Unable to update favorites. Please try again.",
"favoriteNotSupported": "Favorites not supported for external apps!",
Expand All @@ -94,6 +97,8 @@
"imageLabel": "Image",
"inputDescHelpText": "For example, number of files required or file format(s) accepted.",
"inputDescLabel": "Briefly describe input file(s) required by this app.",
"instantLaunchCreate": "Create Instant Launch",
"instantLaunchCreateError": "Error creating instant launch.",
"instantLaunches": "Instant Launches",
"intEmail": "Integrator email",
"intName": "Integrator name",
Expand Down Expand Up @@ -146,6 +151,7 @@
"savedLaunchDeleteConfirmation": "Do you wish to delete this Saved Launch? Note: Deleting a public Saved Launch will break embedded and shared links.",
"savedLaunchDeleteError": "Unable to delete {{name}}.",
"savedLaunchEmbedToolTip": "Copy embed code",
"savedLaunchEmbedUseInstantLaunch": "Use Instant Launch (if available)",
"savedLaunchError": "Unable to get Saved Launch.",
"savedLaunchNameLabel": "Name",
"savedLaunchNotSupportedMessage": "Saved Launch not supported for Agave apps!",
Expand Down
1 change: 1 addition & 0 deletions src/common/NavigationConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export default {
DOI: "doi",
ERROR: "error",
HELP: "help",
INSTANT_LAUNCH: "instantlaunch",
INSTANT_LAUNCHES: "instantlaunches",
LOGIN: "login",
LOGOUT: "logout",
Expand Down
142 changes: 123 additions & 19 deletions src/components/apps/savedLaunch/SavedLaunchListing.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*
*/

import React, { useState } from "react";
import React, { useState, useEffect, useCallback } from "react";
import { useTranslation } from "i18n";
import { useQueryClient, useQuery, useMutation } from "react-query";
import Link from "next/link";
Expand All @@ -15,7 +15,11 @@ import ids from "../ids";
import constants from "../constants";
import SavedLaunch from "./SavedLaunch";

import { getSavedLaunchPath } from "components/apps/utils";
import {
getSavedLaunchPath,
getInstantLaunchPath,
} from "components/apps/utils";
import isQueryLoading from "components/utils/isQueryLoading";
import SystemIds from "components/models/systemId";
import { getHost } from "components/utils/getHost";
import ConfirmationDialog from "components/utils/ConfirmationDialog";
Expand All @@ -29,23 +33,33 @@ import {
listSavedLaunches,
deleteSavedLaunch,
} from "serviceFacades/savedLaunches";
import {
ALL_INSTANT_LAUNCHES_KEY,
listFullInstantLaunches,
addInstantLaunch,
} from "serviceFacades/instantlaunches";

import { useConfig } from "contexts/config";
import { useUserProfile } from "contexts/userProfile";

import buildID from "components/utils/DebugIDUtil";
import CopyTextArea from "components/copy/CopyTextArea";

import Button from "@mui/material/Button";
import Dialog from "@mui/material/Dialog";
import DialogTitle from "@mui/material/DialogTitle";
import DialogContent from "@mui/material/DialogContent";
import FormControlLabel from "@mui/material/FormControlLabel";
import Grid from "@mui/material/Grid";
import IconButton from "@mui/material/IconButton";
import Paper from "@mui/material/Paper";
import Popover from "@mui/material/Popover";
import Switch from "@mui/material/Switch";
import Typography from "@mui/material/Typography";
import { Link as MuiLink } from "@mui/material";
import { useTheme } from "@mui/material";

import Add from "@mui/icons-material/Add";
import Code from "@mui/icons-material/Code";
import Play from "@mui/icons-material/PlayArrow";
import Share from "@mui/icons-material/Share";
Expand Down Expand Up @@ -182,7 +196,10 @@ function ListSavedLaunches(props) {
const [anchorEl, setAnchorEl] = useState(null);
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
const [selected, setSelected] = useState();
const [selectedIL, setSelectedIL] = useState();
const [deleteError, setDeleteError] = useState();
const [useInstantLaunch, setUseInstantLaunch] = useState(false);
const [createILError, setCreateILError] = useState();

const userName = userProfile?.id;

Expand All @@ -198,7 +215,24 @@ function ListSavedLaunches(props) {
queryFn: () => listSavedLaunches({ appId }),
});

// It would be better to have a query that is limited to the selected app only
const {
data: allILs,
error: errorILs,
isFetching: fetchingILs,
} = useQuery(ALL_INSTANT_LAUNCHES_KEY, listFullInstantLaunches);

const savedLaunchClickHandler = (event, savedLaunch) => {
const instantLaunch = allILs?.instant_launches.find(
(el) => el.quick_launch_id === savedLaunch.id
);
if (instantLaunch) {
setSelectedIL(instantLaunch);
setUseInstantLaunch(true);
} else {
setSelectedIL(null);
setUseInstantLaunch(false);
}
setSelected(savedLaunch);
if (savedLaunch.is_public) {
setAnchorEl(event.currentTarget);
Expand All @@ -218,50 +252,68 @@ function ListSavedLaunches(props) {
}
);

const embedCodeClickHandler = () => {
const shareUrl = getShareUrl(selected.id);
const { mutate: createInstantLaunch } = useMutation(addInstantLaunch, {
onSuccess: (createdIL, { onSuccess }) => {
queryClient.invalidateQueries(ALL_INSTANT_LAUNCHES_KEY);
setSelectedIL(createdIL);
setUseInstantLaunch(true);
},
onError: setCreateILError,
});

const getShareUrl = useCallback(() => {
const host = getHost();
const url = `${host}${getSavedLaunchPath(
systemId,
appId,
selected?.id
)}`;
return url;
}, [selected, systemId, appId]);

useEffect(() => {
let shareUrl = "";
const host = getHost();
const imgSrc = `${host}/${constants.SAVED_LAUNCH_EMBED_ICON}`;

if (useInstantLaunch && selectedIL?.id) {
shareUrl = `${host}${getInstantLaunchPath(selectedIL?.id)}`;
} else {
shareUrl = getShareUrl();
}

const embed = `<a href="${shareUrl}" target="_blank" rel="noopener noreferrer"><img src="${imgSrc}"></a>`;

setAnchorEl(null);
setEmbedCode(embed);
}, [selected, selectedIL, useInstantLaunch, getShareUrl]);

const embedCodeClickHandler = () => {
setAnchorEl(null);
setEmbedDialogOpen(true);
};

const shareClickHandler = () => {
setAnchorEl(null);
setSavedLaunchUrl(getShareUrl(selected.id));
setSavedLaunchUrl(getShareUrl());
setShareDialogOpen(true);
};

const getShareUrl = () => {
const host = getHost();
const url = `${host}${getSavedLaunchPath(
systemId,
appId,
selected?.id
)}`;
return url;
};

const deleteSavedLaunchHandler = (event, savedLaunch) => {
setSelected(savedLaunch);
setDeleteConfirmOpen(true);
};

if (error) {
if (error || errorILs) {
return (
<ErrorTypographyWithDialog
baseId={baseDebugId}
errorMessage={t("savedLaunchError")}
errorObject={error}
errorObject={error || errorILs}
/>
);
}

if (isFetching) {
if (isQueryLoading([isFetching, fetchingILs])) {
return <GridLoading rows={3} baseId={baseDebugId} />;
}

Expand Down Expand Up @@ -381,6 +433,58 @@ function ListSavedLaunches(props) {
</IconButton>
</DialogTitle>
<DialogContent>
{!!selectedIL ? (
<FormControlLabel
disabled={!selectedIL}
control={
<Switch
size="small"
checked={useInstantLaunch}
onChange={(event) =>
setUseInstantLaunch(
event.target.checked
)
}
name={t(
"savedLaunchEmbedUseInstantLaunch"
)}
/>
}
label={
<Typography variant="body2">
{t("savedLaunchEmbedUseInstantLaunch")}
</Typography>
}
/>
) : (
<Button
variant="contained"
startIcon={<Add />}
onClick={(event) => {
event.stopPropagation();
event.preventDefault();
createInstantLaunch(selected.id);
}}
>
{t("instantLaunchCreate")}
</Button>
)}
{createILError && (
<ErrorTypographyWithDialog
baseId={baseDebugId}
errorMessage={t("instantLaunchCreateError")}
errorObject={createILError}
/>
)}
<Typography variant="body2">
{!selectedIL && t("embedNoInstantLaunch")}
{selectedIL &&
useInstantLaunch &&
t("embedInstantLaunchUsed")}
{selectedIL &&
!useInstantLaunch &&
t("embedInstantLaunchNotUsed")}
</Typography>
<CopyTextArea
debugIdPrefix={buildID(
baseDebugId,
Expand Down
15 changes: 15 additions & 0 deletions src/components/apps/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,21 @@ export const getAppLaunchPath = (systemId, appId, versionId) =>
export const getSavedLaunchPath = (systemId, appId, launchId) =>
`/${NavigationConstants.APPS}/${systemId}/${appId}/launch?saved-launch-id=${launchId}`;

/**
* Builds a path to the direct instant launch page.
*
* @param {string} ilId The Instant Launch ID
* @param {string} resourcePath A file or folder path to pass to the instant launch (optional)
*/
export const getInstantLaunchPath = (ilId, resourcePath = "") => {
const uri = `/${NavigationConstants.INSTANT_LAUNCH}/${ilId}`;
if (resourcePath) {
return uri + `?resource=${resourcePath}`;
} else {
return uri;
}
};

/**
* Builds a path to the App Editor for the app with the given IDs.
*
Expand Down
4 changes: 2 additions & 2 deletions src/components/instantlaunches/admin/InstantLaunchList.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
DASHBOARD_INSTANT_LAUNCHES_KEY,
addToDashboardHandler,
removeFromDashboardHandler,
deleteInstantLaunchHandler,
adminDeleteInstantLaunchHandler,
addToNavDrawer,
removeFromNavDrawer,
LIST_INSTANT_LAUNCHES_BY_METADATA_KEY,
Expand Down Expand Up @@ -312,7 +312,7 @@ const InstantLaunchList = ({ showErrorAnnouncer }) => {
});

const { mutate: deleteIL, status: deleteILStatus } = useMutation(
deleteInstantLaunchHandler,
adminDeleteInstantLaunchHandler,
{
onSuccess: () => {
queryClient.invalidateQueries(DASHBOARD_INSTANT_LAUNCHES_KEY);
Expand Down
4 changes: 2 additions & 2 deletions src/components/instantlaunches/admin/SavedLaunchList.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
LIST_PUBLIC_SAVED_LAUNCHES_KEY,
getPublicSavedLaunches,
listFullInstantLaunches,
addInstantLaunch,
adminAddInstantLaunch,
} from "serviceFacades/instantlaunches";

import WrappedErrorHandler from "components/error/WrappedErrorHandler";
Expand Down Expand Up @@ -52,7 +52,7 @@ const SavedLaunchList = ({ showErrorAnnouncer }) => {
const queryClient = useQueryClient();
const allILs = useQuery(ALL_INSTANT_LAUNCHES_KEY, listFullInstantLaunches);

const { mutate: promote } = useMutation(addInstantLaunch, {
const { mutate: promote } = useMutation(adminAddInstantLaunch, {
onSuccess: () => {
queryClient.invalidateQueries(ALL_INSTANT_LAUNCHES_KEY);
announce({
Expand Down
26 changes: 26 additions & 0 deletions src/server/api/instantlaunches.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,19 @@ export default () => {
})
);

logger.info("adding the PUT /instantlaunches handler");
api.put(
"/instantlaunches",
auth.authnTokenMiddleware,
terrainHandler({
method: "PUT",
pathname: "/instantlaunches",
headers: {
"Content-Type": "application/json",
},
})
);

logger.info("adding the GET /admin/instantlaunches/metadata/full handler");
api.get(
"/admin/instantlaunches/metadata/full",
Expand All @@ -126,6 +139,19 @@ export default () => {
})
);

logger.info("add the DELETE /instantlaunches/:id handler");
api.delete(
"/instantlaunches/:id",
auth.authnTokenMiddleware,
terrainHandler({
method: "DELETE",
pathname: "/instantlaunches/:id",
headers: {
"Content-Type": "application/json",
},
})
);

logger.info("add the DELETE /admin/instantlaunches/:id handler");
api.delete(
"/admin/instantlaunches/:id",
Expand Down
Loading

0 comments on commit 6e9cb1b

Please sign in to comment.