Skip to content

Commit

Permalink
feat(desktop): display authentication status for pro instances; if no…
Browse files Browse the repository at this point in the history
…t authenticated, clicking on icon will trigger relogin
  • Loading branch information
pascalbreuninger committed Feb 8, 2024
1 parent 0c7b98f commit 768429c
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 28 deletions.
1 change: 1 addition & 0 deletions desktop/src/client/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,6 @@ export const DEVPOD_FLAG_TIMEOUT = "--timeout"
export const DEVPOD_FLAG_DEVCONTAINER_PATH = "--devcontainer-path"
export const DEVPOD_FLAG_WORKSPACE_ID = "--workspace-id"
export const DEVPOD_FLAG_WORKSPACE_UID = "--workspace-uid"
export const DEVPOD_FLAG_LOGIN = "--login"

export const DEVPOD_UI_ENV_VAR = "DEVPOD_UI"
6 changes: 3 additions & 3 deletions desktop/src/client/pro/client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Result, ResultError } from "../../lib"
import { TImportWorkspaceConfig, TProID, TProInstance } from "../../types"
import { TImportWorkspaceConfig, TListProInstancesConfig, TProID, TProInstance } from "../../types"
import { TDebuggable, TStreamEventListenerFn } from "../types"
import { ProCommands } from "./proCommands"

Expand All @@ -19,8 +19,8 @@ export class ProClient implements TDebuggable {
return ProCommands.Login(host, providerName, accessKey, listener)
}

public async listAll(): Promise<Result<readonly TProInstance[]>> {
return ProCommands.ListProInstances()
public async listAll(config: TListProInstancesConfig): Promise<Result<readonly TProInstance[]>> {
return ProCommands.ListProInstances(config)
}

public async remove(id: TProID) {
Expand Down
9 changes: 7 additions & 2 deletions desktop/src/client/pro/proCommands.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Result, ResultError, Return, getErrorFromChildProcess } from "@/lib"
import { TImportWorkspaceConfig, TProID, TProInstance } from "@/types"
import { TImportWorkspaceConfig, TListProInstancesConfig, TProID, TProInstance } from "@/types"
import { Command, isOk, serializeRawOptions, toFlagArg } from "../command"
import {
DEVPOD_COMMAND_DELETE,
Expand All @@ -11,6 +11,7 @@ import {
DEVPOD_FLAG_DEBUG,
DEVPOD_FLAG_JSON_LOG_OUTPUT,
DEVPOD_FLAG_JSON_OUTPUT,
DEVPOD_FLAG_LOGIN,
DEVPOD_FLAG_PROVIDER,
DEVPOD_FLAG_USE,
DEVPOD_FLAG_WORKSPACE_ID,
Expand Down Expand Up @@ -62,11 +63,15 @@ export class ProCommands {
}
}

static async ListProInstances(): Promise<Result<readonly TProInstance[]>> {
static async ListProInstances(
config: TListProInstancesConfig
): Promise<Result<readonly TProInstance[]>> {
const maybeLoginFlag = config?.authenticate ? [DEVPOD_FLAG_LOGIN] : []
const result = await ProCommands.newCommand([
DEVPOD_COMMAND_PRO,
DEVPOD_COMMAND_LIST,
DEVPOD_FLAG_JSON_OUTPUT,
...maybeLoginFlag,
]).run()
if (result.err) {
return result
Expand Down
67 changes: 49 additions & 18 deletions desktop/src/components/Layout/Pro.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { client } from "@/client"
import { useProInstances, useSettings, useWorkspaces } from "@/contexts"
import { Briefcase, DevPodProBadge, Plus } from "@/icons"
import { Briefcase, CheckCircle, DevPodProBadge, ExclamationTriangle, Plus } from "@/icons"
import { exists } from "@/lib"
import { TProID, TProInstance } from "@/types"
import { useLoginProModal } from "@/views/ProInstances/useLoginProModal"
import { useLoginProModal, useReLoginProModal } from "@/views/ProInstances/useLoginProModal"
import { useDeleteProviderModal } from "@/views/Providers/useDeleteProviderModal"
import { ChevronDownIcon, CloseIcon } from "@chakra-ui/icons"
import {
Expand Down Expand Up @@ -35,6 +35,7 @@ import { IconTag } from "../Tag"
export function Pro() {
const [[proInstances]] = useProInstances()
const { modal: loginProModal, handleOpenLogin: handleConnectClicked } = useLoginProModal()
const { modal: reLoginProModal, handleOpenLogin: handleReLoginClicked } = useReLoginProModal()
const [isDeleting, setIsDeleting] = useState(false)

const backgroundColor = useColorModeValue("white", "gray.900")
Expand Down Expand Up @@ -101,24 +102,30 @@ export function Pro() {
</ButtonGroup>
</VStack>
) : (
proInstances.map(
(proInstance) =>
proInstance.host && (
<ProInstaceRow
key={proInstance.host}
{...proInstance}
host={proInstance.host}
onIsDeletingChanged={setIsDeleting}
/>
)
)
proInstances.map((proInstance) => {
const host = proInstance.host
if (!host) {
return null
}

return (
<ProInstanceRow
key={host}
{...proInstance}
host={host}
onIsDeletingChanged={setIsDeleting}
onLoginClicked={() => handleReLoginClicked({ host })}
/>
)
})
)}
</Box>
</PopoverBody>
</PopoverContent>
</Portal>
</Popover>
{loginProModal}
{reLoginProModal}
</>
) : (
<Button
Expand All @@ -131,12 +138,18 @@ export function Pro() {
}

type TProInstaceRowProps = Omit<TProInstance, "host"> &
Readonly<{ host: TProID; onIsDeletingChanged: (isDeleting: boolean) => void }>
function ProInstaceRow({
Readonly<{
host: TProID
onIsDeletingChanged: (isDeleting: boolean) => void
onLoginClicked?: VoidFunction
}>
function ProInstanceRow({
host,
creationTimestamp,
onIsDeletingChanged,
provider,
authenticated,
onLoginClicked,
}: TProInstaceRowProps) {
const [, { disconnect }] = useProInstances()
const workspaces = useWorkspaces()
Expand All @@ -157,15 +170,33 @@ function ProInstaceRow({
)
useEffect(() => {
onIsDeletingChanged(isOpen)
// `onIsDeletingChanged` is expectd to be a stable reference
// `onIsDeletingChanged` is expected to be a stable reference
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen])

return (
<>
<HStack width="full" padding="2" justifyContent="space-between">
<VStack align="start" spacing="0" fontSize="sm">
<Text fontWeight="bold">{host}</Text>
<HStack>
<Text fontWeight="bold">{host}</Text>
{exists(authenticated) && (
<IconTag
variant="ghost"
icon={
authenticated ? (
<CheckCircle color={"green.300"} />
) : (
<ExclamationTriangle color="orange.300" />
)
}
label=""
paddingInlineStart="0"
infoText={authenticated ? "Authenticated" : "Not Authenticated"}
{...(authenticated ? {} : { onClick: onLoginClicked, cursor: "pointer" })}
/>
)}
</HStack>
<HStack>
<IconTag
variant="ghost"
Expand All @@ -178,7 +209,7 @@ function ProInstaceRow({
<IconTag
variant="ghost"
icon={<Icon as={HiClock} />}
label={dayjs(new Date(creationTimestamp)).fromNow()}
label={dayjs(new Date(creationTimestamp)).format("MMM D, YY")}
infoText={`Created ${dayjs(new Date(creationTimestamp)).fromNow()}`}
/>
)}
Expand Down
2 changes: 1 addition & 1 deletion desktop/src/contexts/DevPodContext/DevPodProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function DevPodProvider({ children }: Readonly<{ children?: ReactNode }>)

const proInstancesQuery = useQuery({
queryKey: QueryKeys.PRO_INSTANCES,
queryFn: async () => (await client.pro.listAll()).unwrap(),
queryFn: async () => (await client.pro.listAll({ authenticate: true })).unwrap(),
refetchInterval: REFETCH_INTERVAL_MS,
})

Expand Down
7 changes: 7 additions & 0 deletions desktop/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ export type TProInstance = Readonly<{
host: TMaybe<string>
provider: TMaybe<string>
creationTimestamp: TMaybe<string>
authenticated: TMaybe<boolean>
}>
export type TProInstances = readonly TProInstance[]
export type TProInstanceManager = Readonly<{
Expand All @@ -211,6 +212,12 @@ export type TProInstanceLoginConfig = Readonly<{
accessKey?: string
streamListener?: TStreamEventListenerFn
}>
export type TListProInstancesConfig = Readonly<
| {
authenticate?: boolean
}
| undefined
>
//#endregion

export type TDevcontainerSetup = Readonly<{
Expand Down
2 changes: 1 addition & 1 deletion desktop/src/views/ProInstances/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { useLoginProModal } from "./useLoginProModal"
export { useLoginProModal, useReLoginProModal } from "./useLoginProModal"
66 changes: 63 additions & 3 deletions desktop/src/views/ProInstances/useLoginProModal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BottomActionBar, BottomActionBarError, Form, useStreamingTerminal } from "@/components"
import { useProInstances, useProviders } from "@/contexts"
import { useProInstanceManager, useProInstances, useProviders } from "@/contexts"
import { exists, useFormErrors } from "@/lib"
import { Routes } from "@/routes"
import {
Expand All @@ -19,6 +19,7 @@ import {
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Tooltip,
Expand Down Expand Up @@ -153,7 +154,7 @@ export function useLoginProModal() {
<ModalOverlay />
<ModalContent overflow="hidden">
{login.status !== "loading" && <ModalCloseButton />}
<ModalHeader>Connect to Loft DevPod Pro</ModalHeader>
<ModalHeader>Connect to DevPod Pro</ModalHeader>
<ModalBody overflowX="hidden" overflowY="auto" paddingBottom="0" ref={containerRef}>
<VStack align="start" spacing="8" paddingX="4" paddingTop="4">
<Form onSubmit={handleSubmit(onSubmit)} justifyContent="center">
Expand Down Expand Up @@ -204,7 +205,7 @@ export function useLoginProModal() {
<FormErrorMessage>{proURLError.message}</FormErrorMessage>
) : (
<FormHelperText>
Enter a URL to the Loft DevPod Pro instance you intend to connect to. If
Enter a URL to the DevPod Pro instance you intend to connect to. If
you&apos;re unsure about it, ask your company administrator or create a new
Pro instance on your local machine.
</FormHelperText>
Expand Down Expand Up @@ -320,3 +321,62 @@ export function useLoginProModal() {

return { modal, handleOpenLogin }
}

export function useReLoginProModal() {
const { terminal, connectStream, clear: clearTerminal } = useStreamingTerminal({ fontSize: "sm" })
const { login } = useProInstanceManager()
const { isOpen, onClose, onOpen } = useDisclosure()
const containerRef = useRef<HTMLDivElement>(null)

const handleOpenLogin = useCallback(
(data: NonNullable<Pick<TSetupProInitialData, "host">>) => {
onOpen()
login.run({ host: data.host, streamListener: connectStream })
},
[connectStream, login, onOpen]
)

const resetModal = useCallback(() => {
clearTerminal()
onClose()
}, [clearTerminal, onClose])

const modal = useMemo(() => {
return (
<Modal
onClose={resetModal}
isOpen={isOpen}
closeOnEsc={login.status !== "loading"}
closeOnOverlayClick={login.status !== "loading"}
isCentered
size="4xl"
scrollBehavior="inside">
<ModalOverlay />
<ModalContent overflow="hidden">
{login.status !== "loading" && <ModalCloseButton />}
<ModalHeader>Login to DevPod Pro</ModalHeader>
<ModalBody overflowX="hidden" overflowY="auto" paddingBottom="0" ref={containerRef}>
<VStack align="start" spacing="8" paddingX="4" paddingTop="4" paddingBottom="6">
{login.status !== "idle" && (
<Box width="full" height="10rem">
{terminal}
</Box>
)}
</VStack>
</ModalBody>
<ModalFooter>
<Button
isDisabled={login.status !== "success"}
isLoading={login.status === "loading"}
variant="solid"
onClick={resetModal}>
Done
</Button>
</ModalFooter>
</ModalContent>
</Modal>
)
}, [resetModal, isOpen, login.status, terminal])

return { modal, handleOpenLogin }
}

0 comments on commit 768429c

Please sign in to comment.