diff --git a/src/common/services/userapps.service.ts b/src/common/services/userapps.service.ts index 44ecd09..09ae75f 100644 --- a/src/common/services/userapps.service.ts +++ b/src/common/services/userapps.service.ts @@ -3,12 +3,28 @@ */ -import { V1 } from "."; +import {handleError, V1} from "."; +import ReactGA from "react-ga"; export const deepCopy = (obj: any) => { return JSON.parse(JSON.stringify(obj)); } +export const installUserapp = (appSpec: V1.Service, allSpecs: Array): Promise => { + const userApp: V1.Stack = newStack(appSpec, allSpecs); + + // POST /stacks + return V1.UserAppService.createUserapp(userApp).then(stk => { + ReactGA.event({ + category: 'application', + action: 'add', + label: stk.key + }); + + return stk; + }).catch(reason => handleError(`Failed to add ${userApp.key} user app`, reason)) +} + export const newSpec = (): V1.Service => ({ // Basic Info key: '', diff --git a/src/views/all-apps/SpecCard.tsx b/src/views/all-apps/SpecCard.tsx index 9ce7cd8..c3bf16b 100644 --- a/src/views/all-apps/SpecCard.tsx +++ b/src/views/all-apps/SpecCard.tsx @@ -18,10 +18,11 @@ import {faPlus} from "@fortawesome/free-solid-svg-icons/faPlus"; // Custom helpers import Taglist from "./Taglist"; -import {newStack, copySpec, CONFLICT_409, handleError, V1} from "../../common/services"; +import {copySpec, CONFLICT_409, handleError, V1} from "../../common/services"; import '../../index.css'; import './SpecCard.css'; +import {installUserapp} from "../../common/services/userapps.service"; // TODO: Abstract this? @@ -47,18 +48,13 @@ function SpecCard(props: CardProps) { const installApplication = (): void => { const appSpec = props.spec; - const userApp: V1.Stack = newStack(appSpec, props.specs); - - // POST /stacks - V1.UserAppService.createUserapp(userApp).then(stk => { - ReactGA.event({ - category: 'application', - action: 'add', - label: stk.key - }); - props.stacks.push(stk); - setRedirect(`/my-apps`); - }).catch(reason => handleError(`Failed to add ${userApp.key} user app`, reason)); + const allSpecs = props.specs; + installUserapp(appSpec, allSpecs).then(stk => { + if (stk) { + props.stacks.push(stk); + setRedirect(`/my-apps`); + } + }); } const cloneSpec = (): void => { diff --git a/src/views/all-apps/SpecView.tsx b/src/views/all-apps/SpecView.tsx index bc5f9c6..d88eba2 100644 --- a/src/views/all-apps/SpecView.tsx +++ b/src/views/all-apps/SpecView.tsx @@ -11,7 +11,7 @@ import {useSelector} from "react-redux"; import ReactGA from "react-ga"; import './SpecView.css'; -import {newStack} from "../../common/services/userapps.service"; +import {installUserapp} from "../../common/services/userapps.service"; import {faPlus} from "@fortawesome/free-solid-svg-icons/faPlus"; type SpecViewParams = { @@ -36,17 +36,11 @@ function SpecView() { if (!appSpec) { return; } - const userApp: V1.Stack = newStack(appSpec, specs); - - // POST /stacks - V1.UserAppService.createUserapp(userApp).then(stk => { - ReactGA.event({ - category: 'application', - action: 'add', - label: stk.key - }); - setRedirect(`/my-apps`); - }).catch(reason => handleError(`Failed to add ${userApp.key} user app`, reason)); + installUserapp(appSpec, specs).then(stk => { + if (stk) { + setRedirect(`/my-apps`); + } + }); } useEffect(() => { diff --git a/src/views/my-apps/MyApps.tsx b/src/views/my-apps/MyApps.tsx index 403aeaf..fa8a18d 100644 --- a/src/views/my-apps/MyApps.tsx +++ b/src/views/my-apps/MyApps.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useState} from 'react'; +import React, {useCallback, useEffect, useState} from 'react'; import '../../index.css'; import Accordion from "react-bootstrap/Accordion"; import Button from "react-bootstrap/Button"; @@ -32,9 +32,13 @@ import {colors} from "../../App"; import ReactGA from "react-ga"; import './MyApps.css'; +import {StringParam, useQueryParam} from "use-query-params"; +import {installUserapp} from "../../common/services/userapps.service"; const navigate = (stk: V1.Stack, ep: any) => { - window.open(`${ep.url}`, '_blank'); + if (ep?.url) { + window.open(ep.url, '_blank'); + } } const sortBy = (s1: V1.Stack, s2: V1.Stack) => { @@ -59,6 +63,9 @@ function MyAppsPage(props: any) { const env = useSelector((state: any) => state.env); const user = useSelector((state: any) => state.auth.user); + const [quickstart, setQuickstart] = useQueryParam('quickstart', StringParam); + const [quickStartThread, setQuickStartThread] = useState(undefined); + // Server data const [stacks, setStacks] = useState>([]); const [specs, setSpecs] = useState>([]); @@ -91,6 +98,25 @@ function MyAppsPage(props: any) { } }, [env?.analytics_tracking_id]); + const startStack = useCallback((stack: V1.Stack) => { + const stackId = stack.id + ""; + return V1.UserAppService.startStack(stackId) + .catch(reason => handleError("Failed to start stack", reason)) + .then((resp) => { + if (!resp) return; + + if (env?.auth?.gaTrackingId) { + ReactGA.event({ + category: 'application', + action: 'start', + label: stack.key + }); + } + console.log("Stack is now starting..."); + refresh(); + }); + }, [env]); + useEffect(() => { const transient = stacks.filter(stk => stk?.status?.endsWith('ing')); if (transient.length && !autoRefresh) { @@ -108,7 +134,45 @@ function MyAppsPage(props: any) { setRefreshInterval(undefined); } } - }, [autoRefresh, refreshInterval, stacks, stacks.length]); + + // If quickstart queryParam provided, start and navigate to the chosen app + if (quickstart) { + console.log('Quick-starting app: ', quickstart); + const existing = stacks.find(s => s.key === quickstart); + + if (existing && existing?.status === 'started') { + const service = existing.services?.find(svc => svc.endpoints?.length); + const endpoint = service?.endpoints?.find(ep => ep.host); + navigate(existing, endpoint); + setQuickstart(undefined); + } else if (existing && existing?.status === 'stopped') { + console.log('Existing app found! Starting existing app: ', existing); + startStack(existing); + } else if (!existing && !quickStartThread) { + const timeout = setTimeout(() => { + // Stack exists for this app, start it up + console.log('Checking all specs to create new app: ', specs); + + // Otherwise install a new app and start that up + const appSpec = specs.find(s => s.key === quickstart); + console.log('Found spec to create new app: ', appSpec); + + if (!appSpec) { + console.log('No spec found, aborting: ', appSpec); + return; + } + console.log('Installing new app from spec: ', appSpec); + installUserapp(appSpec, specs).then(stk => { + if (stk) { + console.log('Starting new app: ', stk); + startStack(stk); + } + }); + }, 3000); + setQuickStartThread(timeout); + } + } + }, [autoRefresh, refreshInterval, stacks, stacks.length, quickstart, setQuickstart, quickStartThread, specs, startStack]); useEffect(() => { if (!Object.keys(env).length) return; @@ -153,25 +217,6 @@ function MyAppsPage(props: any) { }); } - const startStack = (stack: V1.Stack) => { - const stackId = stack.id + ""; - return V1.UserAppService.startStack(stackId) - .catch(reason => handleError("Failed to start stack", reason)) - .then((resp) => { - if (!resp) return; - - if (env?.auth?.gaTrackingId) { - ReactGA.event({ - category: 'application', - action: 'start', - label: stack.key - }); - } - console.log("Stack is now starting..."); - refresh(); - }); - } - const stopStack = (stack: V1.Stack) => { const stackId = stack.id + ""; return V1.UserAppService.stopStack(stackId)