Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide ability to copy URL to access an internally accessible resource #3420

Merged
merged 15 commits into from
Apr 17, 2023
Merged
2 changes: 1 addition & 1 deletion ui/app/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tre-ui",
"version": "0.5.1",
"version": "0.5.2",
"private": true,
"dependencies": {
"@azure/msal-browser": "^2.35.0",
Expand Down
60 changes: 60 additions & 0 deletions ui/app/src/components/shared/ConfirmCopyUrlToClipboard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Dialog, PrimaryButton, DialogType, Stack, TooltipHost, TextField } from '@fluentui/react';
import React, { useState } from 'react';
import { Resource } from '../../models/resource';

interface ConfirmCopyUrlToClipboardProps {
resource: Resource,
onDismiss: () => void
}

// show a explanation about why connect is disabled, and show a copy to clipboard tool tip
export const ConfirmCopyUrlToClipboard: React.FunctionComponent<ConfirmCopyUrlToClipboardProps> = (props: ConfirmCopyUrlToClipboardProps) => {
const COPY_TOOL_TIP_DEFAULT_MESSAGE = "Copy to clipboard"

const [copyToolTipMessage, setCopyToolTipMessage] = useState<string>(COPY_TOOL_TIP_DEFAULT_MESSAGE);

const copyUrlToClipboardProps = {
type: DialogType.normal,
title: 'Access a Protected Endpoint',
closeButtonAriaLabel: 'Close',
subText: `Copy the link below, paste it and use it from a workspace virtual machine`,
};

const dialogStyles = { main: { maxWidth: 450 } };
const modalProps = {
titleAriaId: 'labelId',
subtitleAriaId: 'subTextId',
isBlocking: true,
styles: dialogStyles
};

const handleCopyUrl = () => {
navigator.clipboard.writeText(props.resource.properties.connection_uri);
setCopyToolTipMessage("Copied")
setTimeout(() => setCopyToolTipMessage(COPY_TOOL_TIP_DEFAULT_MESSAGE), 3000);
}

return (<>
<Dialog
hidden={false}
onDismiss={() => props.onDismiss()}
dialogContentProps={copyUrlToClipboardProps}
modalProps={modalProps}
>
<Stack horizontal styles={{ root: { alignItems: 'center', paddingTop: '7px' } }}>
<Stack.Item grow>
<TextField readOnly value={props.resource.properties.connection_uri} />
</Stack.Item>
<TooltipHost content={copyToolTipMessage}>
<PrimaryButton
iconProps={{ iconName: 'copy' }}
styles={{ root: { minWidth: '40px' } }}
onClick={() => { handleCopyUrl() }}
/>
</TooltipHost>
</Stack>
</Dialog>

</>);
};

8 changes: 7 additions & 1 deletion ui/app/src/components/shared/ResourceCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { PowerStateBadge } from './PowerStateBadge';
import { ResourceType } from '../../models/resourceType';
import { WorkspaceContext } from '../../contexts/WorkspaceContext';
import { CostsTag } from './CostsTag';
import { ConfirmCopyUrlToClipboard } from './ConfirmCopyUrlToClipboard';

interface ResourceCardProps {
resource: Resource,
Expand All @@ -19,10 +20,12 @@ interface ResourceCardProps {
onUpdate: (resource: Resource) => void,
onDelete: (resource: Resource) => void,
readonly?: boolean
isExposedExternally?: boolean
}

export const ResourceCard: React.FunctionComponent<ResourceCardProps> = (props: ResourceCardProps) => {
const [loading] = useState(false);
const [showCopyUrl, setShowCopyUrl] = useState(false);
const [showInfo, setShowInfo] = useState(false);
const workspaceCtx = useContext(WorkspaceContext);
const latestUpdate = useComponentManager(
Expand Down Expand Up @@ -139,12 +142,15 @@ export const ResourceCard: React.FunctionComponent<ResourceCardProps> = (props:
<CostsTag resourceId={props.resource.id} />
{
connectUri && <PrimaryButton
onClick={(e) => {e.stopPropagation(); window.open(connectUri)}}
onClick={(e) => {e.stopPropagation(); props.isExposedExternally === false ? setShowCopyUrl(true) : window.open(connectUri)}}
disabled={shouldDisable()}
title={shouldDisable() ? 'Resource must be enabled, successfully deployed & powered on to connect' : 'Connect to resource'}>
Connect
</PrimaryButton>
}
{
showCopyUrl && <ConfirmCopyUrlToClipboard onDismiss={() => setShowCopyUrl(false)} resource={props.resource} />
}
</Stack>
</Stack>
</TooltipHost>
Expand Down
4 changes: 3 additions & 1 deletion ui/app/src/components/shared/ResourceCardList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface ResourceCardListProps {
removeResource: (resource: Resource) => void
emptyText: string,
readonly?: boolean
isExposedExternally?: boolean
}

export const ResourceCardList: React.FunctionComponent<ResourceCardListProps> = (props: ResourceCardListProps) => {
Expand All @@ -30,7 +31,8 @@ export const ResourceCardList: React.FunctionComponent<ResourceCardListProps> =
onUpdate={(resource: Resource) => props.updateResource(resource)}
onDelete={(resource: Resource) => props.removeResource(resource)}
itemId={i}
readonly={props.readonly} />
readonly={props.readonly}
isExposedExternally={r.properties.is_exposed_externally === undefined ? props.isExposedExternally : r.properties.is_exposed_externally} />
</Stack.Item>
)
})
Expand Down
19 changes: 18 additions & 1 deletion ui/app/src/components/shared/ResourceContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ApiEndpoint } from '../../models/apiEndpoints';
import { UserResource } from '../../models/userResource';
import { getActionIcon, ResourceTemplate, TemplateAction } from '../../models/resourceTemplate';
import { ConfirmDeleteResource } from './ConfirmDeleteResource';
import { ConfirmCopyUrlToClipboard } from './ConfirmCopyUrlToClipboard';
import { ConfirmDisableEnableResource } from './ConfirmDisableEnableResource';
import { CreateUpdateResourceContext } from '../../contexts/CreateUpdateResourceContext';
import { Workspace } from '../../models/workspace';
Expand All @@ -31,6 +32,7 @@ export const ResourceContextMenu: React.FunctionComponent<ResourceContextMenuPro
const workspaceCtx = useContext(WorkspaceContext);
const [showDisable, setShowDisable] = useState(false);
const [showDelete, setShowDelete] = useState(false);
const [showCopyUrl, setShowCopyUrl] = useState(false);
const [showUpgrade, setShowUpgrade] = useState(false);
const [resourceTemplate, setResourceTemplate] = useState({} as ResourceTemplate);
const createFormCtx = useContext(CreateUpdateResourceContext);
Expand Down Expand Up @@ -142,7 +144,7 @@ export const ResourceContextMenu: React.FunctionComponent<ResourceContextMenuPro
}

// add 'connect' button if we have a URL to connect to
if (props.resource.properties.connection_uri) {
if (props.resource.properties.is_exposed_externally === true) {
menuItems.push({
key: 'connect',
text: 'Connect',
Expand All @@ -152,6 +154,17 @@ export const ResourceContextMenu: React.FunctionComponent<ResourceContextMenuPro
disabled: shouldDisableConnect()
})
}
else if (props.resource.properties.is_exposed_externally === false) {
menuItems.push({
key: 'connect',
text: 'Connect',
title: shouldDisableConnect() ? 'Resource must be deployed, enabled & powered on to connect' : 'Connect to resource',
iconProps: { iconName: 'PlugConnected' },
onClick: () => setShowCopyUrl(true),
disabled: shouldDisableConnect()
})
}


const shouldDisableActions = () => {
return props.componentAction === ComponentAction.Lock
Expand Down Expand Up @@ -221,6 +234,10 @@ export const ResourceContextMenu: React.FunctionComponent<ResourceContextMenuPro
showDelete &&
<ConfirmDeleteResource onDismiss={() => setShowDelete(false)} resource={props.resource} />
}
{
showCopyUrl &&
<ConfirmCopyUrlToClipboard onDismiss={() => setShowCopyUrl(false)} resource={props.resource} />
}
{
showUpgrade &&
<ConfirmUpgradeResource onDismiss={() => setShowUpgrade(false)} resource={props.resource} />
Expand Down
3 changes: 2 additions & 1 deletion ui/app/src/components/workspaces/WorkspaceServiceItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@ export const WorkspaceServiceItem: React.FunctionComponent<WorkspaceServiceItemP
selectResource={(r: Resource) => setSelectedUserResource(r as UserResource)}
updateResource={(r: Resource) => updateUserResource(r as UserResource)}
removeResource={(r: Resource) => removeUserResource(r as UserResource)}
emptyText="This workspace service contains no user resources." />
emptyText="This workspace service contains no user resources."
isExposedExternally={workspaceService.properties.is_exposed_externally} />
}
</Stack.Item>
</Stack>
Expand Down
2 changes: 1 addition & 1 deletion ui/app/src/models/workspaceService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ import { Resource } from "./resource";

export interface WorkspaceService extends Resource {
workspaceId: string
}
}