mirror of
https://github.com/coder/coder.git
synced 2025-07-21 01:28:49 +00:00
chore: touch ups to wsproxy UX (#8350)
* chore: update wording on wsproxy help * chore: show help if no fields specified in wsproxy edit * chore: Add run command example to wsproxy create * chore: remove localhost warning * chore: navbar match page title * chore: Add helper text to latency picker * chore: add confirm delete to workspace proxy delete cli * chore: add errors + warnings to workspace proxy table
This commit is contained in:
@ -95,7 +95,7 @@ func (*RootCmd) proxyServer() *clibase.Cmd {
|
|||||||
),
|
),
|
||||||
Handler: func(inv *clibase.Invocation) error {
|
Handler: func(inv *clibase.Invocation) error {
|
||||||
if !(primaryAccessURL.Scheme == "http" || primaryAccessURL.Scheme == "https") {
|
if !(primaryAccessURL.Scheme == "http" || primaryAccessURL.Scheme == "https") {
|
||||||
return xerrors.Errorf("primary access URL must be http or https: url=%s", primaryAccessURL.String())
|
return xerrors.Errorf("'--primary-access-url' value must be http or https: url=%s", primaryAccessURL.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
var closers closers
|
var closers closers
|
||||||
@ -175,20 +175,6 @@ func (*RootCmd) proxyServer() *clibase.Cmd {
|
|||||||
defer httpClient.CloseIdleConnections()
|
defer httpClient.CloseIdleConnections()
|
||||||
closers.Add(httpClient.CloseIdleConnections)
|
closers.Add(httpClient.CloseIdleConnections)
|
||||||
|
|
||||||
// Warn the user if the access URL appears to be a loopback address.
|
|
||||||
isLocal, err := cli.IsLocalURL(ctx, cfg.AccessURL.Value())
|
|
||||||
if isLocal || err != nil {
|
|
||||||
reason := "could not be resolved"
|
|
||||||
if isLocal {
|
|
||||||
reason = "isn't externally reachable"
|
|
||||||
}
|
|
||||||
cliui.Warnf(
|
|
||||||
inv.Stderr,
|
|
||||||
"The access URL %s %s, this may cause unexpected problems when creating workspaces. Generate a unique *.try.coder.app URL by not specifying an access URL.\n",
|
|
||||||
cliui.DefaultStyles.Field.Render(cfg.AccessURL.String()), reason,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// A newline is added before for visibility in terminal output.
|
// A newline is added before for visibility in terminal output.
|
||||||
cliui.Infof(inv.Stdout, "\nView the Web UI: %s", cfg.AccessURL.String())
|
cliui.Infof(inv.Stdout, "\nView the Web UI: %s", cfg.AccessURL.String())
|
||||||
|
|
||||||
|
@ -18,8 +18,8 @@ func (r *RootCmd) workspaceProxy() *clibase.Cmd {
|
|||||||
Use: "workspace-proxy",
|
Use: "workspace-proxy",
|
||||||
Short: "Workspace proxies provide low-latency experiences for geo-distributed teams.",
|
Short: "Workspace proxies provide low-latency experiences for geo-distributed teams.",
|
||||||
Long: "Workspace proxies provide low-latency experiences for geo-distributed teams. " +
|
Long: "Workspace proxies provide low-latency experiences for geo-distributed teams. " +
|
||||||
"It will act as a connection gateway to your workspace providing a lower latency solution " +
|
"It will act as a connection gateway to your workspace. " +
|
||||||
"to connecting to your workspace if Coder and your workspace are deployed in different regions.",
|
"Best used if Coder and your workspace are deployed in different regions.",
|
||||||
Aliases: []string{"wsproxy"},
|
Aliases: []string{"wsproxy"},
|
||||||
Hidden: true,
|
Hidden: true,
|
||||||
Handler: func(inv *clibase.Invocation) error {
|
Handler: func(inv *clibase.Invocation) error {
|
||||||
@ -51,6 +51,7 @@ func (r *RootCmd) regenerateProxyToken() *clibase.Cmd {
|
|||||||
),
|
),
|
||||||
Handler: func(inv *clibase.Invocation) error {
|
Handler: func(inv *clibase.Invocation) error {
|
||||||
ctx := inv.Context()
|
ctx := inv.Context()
|
||||||
|
formatter.primaryAccessURL = client.URL.String()
|
||||||
// This is cheeky, but you can also use a uuid string in
|
// This is cheeky, but you can also use a uuid string in
|
||||||
// 'DeleteWorkspaceProxyByName' and it will work.
|
// 'DeleteWorkspaceProxyByName' and it will work.
|
||||||
proxy, err := client.WorkspaceProxyByName(ctx, inv.Args[0])
|
proxy, err := client.WorkspaceProxyByName(ctx, inv.Args[0])
|
||||||
@ -120,6 +121,7 @@ func (r *RootCmd) patchProxy() *clibase.Cmd {
|
|||||||
Handler: func(inv *clibase.Invocation) error {
|
Handler: func(inv *clibase.Invocation) error {
|
||||||
ctx := inv.Context()
|
ctx := inv.Context()
|
||||||
if proxyIcon == "" && displayName == "" && proxyName == "" {
|
if proxyIcon == "" && displayName == "" && proxyName == "" {
|
||||||
|
_ = inv.Command.HelpHandler(inv)
|
||||||
return xerrors.Errorf("specify at least one field to update")
|
return xerrors.Errorf("specify at least one field to update")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,13 +189,32 @@ func (r *RootCmd) deleteProxy() *clibase.Cmd {
|
|||||||
cmd := &clibase.Cmd{
|
cmd := &clibase.Cmd{
|
||||||
Use: "delete <name|id>",
|
Use: "delete <name|id>",
|
||||||
Short: "Delete a workspace proxy",
|
Short: "Delete a workspace proxy",
|
||||||
|
Options: clibase.OptionSet{
|
||||||
|
cliui.SkipPromptOption(),
|
||||||
|
},
|
||||||
Middleware: clibase.Chain(
|
Middleware: clibase.Chain(
|
||||||
clibase.RequireNArgs(1),
|
clibase.RequireNArgs(1),
|
||||||
r.InitClient(client),
|
r.InitClient(client),
|
||||||
),
|
),
|
||||||
Handler: func(inv *clibase.Invocation) error {
|
Handler: func(inv *clibase.Invocation) error {
|
||||||
ctx := inv.Context()
|
ctx := inv.Context()
|
||||||
err := client.DeleteWorkspaceProxyByName(ctx, inv.Args[0])
|
|
||||||
|
wsproxy, err := client.WorkspaceProxyByName(ctx, inv.Args[0])
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("fetch workspace proxy %q: %w", inv.Args[0], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm deletion of the template.
|
||||||
|
_, err = cliui.Prompt(inv, cliui.PromptOptions{
|
||||||
|
Text: fmt.Sprintf("Delete this workspace proxy: %s?", cliui.DefaultStyles.Code.Render(wsproxy.DisplayName)),
|
||||||
|
IsConfirm: true,
|
||||||
|
Default: cliui.ConfirmNo,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = client.DeleteWorkspaceProxyByName(ctx, inv.Args[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("delete workspace proxy %q: %w", inv.Args[0], err)
|
return xerrors.Errorf("delete workspace proxy %q: %w", inv.Args[0], err)
|
||||||
}
|
}
|
||||||
@ -225,6 +246,7 @@ func (r *RootCmd) createProxy() *clibase.Cmd {
|
|||||||
),
|
),
|
||||||
Handler: func(inv *clibase.Invocation) error {
|
Handler: func(inv *clibase.Invocation) error {
|
||||||
ctx := inv.Context()
|
ctx := inv.Context()
|
||||||
|
formatter.primaryAccessURL = client.URL.String()
|
||||||
var err error
|
var err error
|
||||||
if proxyName == "" && !noPrompts {
|
if proxyName == "" && !noPrompts {
|
||||||
proxyName, err = cliui.Prompt(inv, cliui.PromptOptions{
|
proxyName, err = cliui.Prompt(inv, cliui.PromptOptions{
|
||||||
@ -369,6 +391,7 @@ func (r *RootCmd) listProxies() *clibase.Cmd {
|
|||||||
type updateProxyResponseFormatter struct {
|
type updateProxyResponseFormatter struct {
|
||||||
onlyToken bool
|
onlyToken bool
|
||||||
formatter *cliui.OutputFormatter
|
formatter *cliui.OutputFormatter
|
||||||
|
primaryAccessURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *updateProxyResponseFormatter) Format(ctx context.Context, data codersdk.UpdateWorkspaceProxyResponse) (string, error) {
|
func (f *updateProxyResponseFormatter) Format(ctx context.Context, data codersdk.UpdateWorkspaceProxyResponse) (string, error) {
|
||||||
@ -392,7 +415,8 @@ func (f *updateProxyResponseFormatter) AttachOptions(opts *clibase.OptionSet) {
|
|||||||
func newUpdateProxyResponseFormatter() *updateProxyResponseFormatter {
|
func newUpdateProxyResponseFormatter() *updateProxyResponseFormatter {
|
||||||
up := &updateProxyResponseFormatter{
|
up := &updateProxyResponseFormatter{
|
||||||
onlyToken: false,
|
onlyToken: false,
|
||||||
formatter: cliui.NewOutputFormatter(
|
}
|
||||||
|
up.formatter = cliui.NewOutputFormatter(
|
||||||
// Text formatter should be human readable.
|
// Text formatter should be human readable.
|
||||||
cliui.ChangeFormatterData(cliui.TextFormat(), func(data any) (any, error) {
|
cliui.ChangeFormatterData(cliui.TextFormat(), func(data any) (any, error) {
|
||||||
response, ok := data.(codersdk.UpdateWorkspaceProxyResponse)
|
response, ok := data.(codersdk.UpdateWorkspaceProxyResponse)
|
||||||
@ -400,10 +424,16 @@ func newUpdateProxyResponseFormatter() *updateProxyResponseFormatter {
|
|||||||
return nil, xerrors.Errorf("unexpected type %T", data)
|
return nil, xerrors.Errorf("unexpected type %T", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("Workspace Proxy %q updated successfully.\n"+
|
return fmt.Sprintf("Workspace Proxy %[1]q updated successfully.\n"+
|
||||||
cliui.DefaultStyles.Placeholder.Render("—————————————————————————————————————————————————")+"\n"+
|
cliui.DefaultStyles.Placeholder.Render("—————————————————————————————————————————————————")+"\n"+
|
||||||
"Save this authentication token, it will not be shown again.\n"+
|
"Save this authentication token, it will not be shown again.\n"+
|
||||||
"Token: %s\n", response.Proxy.Name, response.ProxyToken), nil
|
"Token: %[2]s\n"+
|
||||||
|
"\n"+
|
||||||
|
"Start the proxy by running:\n"+
|
||||||
|
cliui.DefaultStyles.Code.Render("CODER_PROXY_SESSION_TOKEN=%[2]s coder wsproxy server --primary-access-url %[3]s --http-address=0.0.0.0:3001")+
|
||||||
|
// This is required to turn off the code style. Otherwise it appears in the code block until the end of the line.
|
||||||
|
cliui.DefaultStyles.Placeholder.Render(""),
|
||||||
|
response.Proxy.Name, response.ProxyToken, up.primaryAccessURL), nil
|
||||||
}),
|
}),
|
||||||
cliui.JSONFormat(),
|
cliui.JSONFormat(),
|
||||||
// Table formatter expects a slice, make a slice of one.
|
// Table formatter expects a slice, make a slice of one.
|
||||||
@ -415,8 +445,7 @@ func newUpdateProxyResponseFormatter() *updateProxyResponseFormatter {
|
|||||||
}
|
}
|
||||||
return []codersdk.UpdateWorkspaceProxyResponse{response}, nil
|
return []codersdk.UpdateWorkspaceProxyResponse{response}, nil
|
||||||
}),
|
}),
|
||||||
),
|
)
|
||||||
}
|
|
||||||
|
|
||||||
return up
|
return up
|
||||||
}
|
}
|
||||||
|
@ -125,7 +125,7 @@ func Test_ProxyCRUD(t *testing.T) {
|
|||||||
|
|
||||||
inv, conf := newCLI(
|
inv, conf := newCLI(
|
||||||
t,
|
t,
|
||||||
"wsproxy", "delete", expectedName,
|
"wsproxy", "delete", "-y", expectedName,
|
||||||
)
|
)
|
||||||
|
|
||||||
pty := ptytest.New(t)
|
pty := ptytest.New(t)
|
||||||
|
@ -84,7 +84,7 @@ export const Sidebar: React.FC = () => {
|
|||||||
href="workspace-proxies"
|
href="workspace-proxies"
|
||||||
icon={<SidebarNavItemIcon icon={HubOutlinedIcon} />}
|
icon={<SidebarNavItemIcon icon={HubOutlinedIcon} />}
|
||||||
>
|
>
|
||||||
Workspace Proxy
|
Workspace Proxies
|
||||||
</SidebarNavItem>
|
</SidebarNavItem>
|
||||||
)}
|
)}
|
||||||
<SidebarNavItem
|
<SidebarNavItem
|
||||||
|
@ -24,6 +24,11 @@ import Skeleton from "@mui/material/Skeleton"
|
|||||||
import { BUTTON_SM_HEIGHT } from "theme/theme"
|
import { BUTTON_SM_HEIGHT } from "theme/theme"
|
||||||
import { ProxyStatusLatency } from "components/ProxyStatusLatency/ProxyStatusLatency"
|
import { ProxyStatusLatency } from "components/ProxyStatusLatency/ProxyStatusLatency"
|
||||||
import { usePermissions } from "hooks/usePermissions"
|
import { usePermissions } from "hooks/usePermissions"
|
||||||
|
import {
|
||||||
|
HelpTooltip,
|
||||||
|
HelpTooltipText,
|
||||||
|
HelpTooltipTitle,
|
||||||
|
} from "components/Tooltips/HelpTooltip"
|
||||||
|
|
||||||
export const USERS_LINK = `/users?filter=${encodeURIComponent("status:active")}`
|
export const USERS_LINK = `/users?filter=${encodeURIComponent("status:active")}`
|
||||||
|
|
||||||
@ -186,6 +191,7 @@ export const NavbarView: FC<NavbarViewProps> = ({
|
|||||||
const ProxyMenu: FC<{ proxyContextValue: ProxyContextValue }> = ({
|
const ProxyMenu: FC<{ proxyContextValue: ProxyContextValue }> = ({
|
||||||
proxyContextValue,
|
proxyContextValue,
|
||||||
}) => {
|
}) => {
|
||||||
|
const styles = useStyles()
|
||||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
const [refetchDate, setRefetchDate] = useState<Date>()
|
const [refetchDate, setRefetchDate] = useState<Date>()
|
||||||
@ -269,6 +275,34 @@ const ProxyMenu: FC<{ proxyContextValue: ProxyContextValue }> = ({
|
|||||||
onClose={closeMenu}
|
onClose={closeMenu}
|
||||||
sx={{ "& .MuiMenu-paper": { py: 1 } }}
|
sx={{ "& .MuiMenu-paper": { py: 1 } }}
|
||||||
>
|
>
|
||||||
|
<MenuItem
|
||||||
|
sx={[
|
||||||
|
{ fontSize: 14 },
|
||||||
|
{ "&:hover": { backgroundColor: "transparent" } },
|
||||||
|
{ wordWrap: "break-word" },
|
||||||
|
{ inlineSize: "200px" },
|
||||||
|
{ whiteSpace: "normal" },
|
||||||
|
{ textAlign: "center" },
|
||||||
|
]}
|
||||||
|
onClick={(e) => {
|
||||||
|
// Stop the menu from closing
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
Reduce workspace latency by selecting the region nearest you.
|
||||||
|
{/* This was always on a newline below the text. This puts it on the same line.
|
||||||
|
It still doesn't look great, but it is marginally better. */}
|
||||||
|
<HelpTooltip buttonClassName={styles.displayInitial}>
|
||||||
|
<HelpTooltipTitle>Workspace Proxy Selection</HelpTooltipTitle>
|
||||||
|
<HelpTooltipText>
|
||||||
|
Only applies to web connections. Local ssh connections will
|
||||||
|
automatically select the nearest region based on latency.
|
||||||
|
</HelpTooltipText>
|
||||||
|
</HelpTooltip>
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
<Divider sx={{ borderColor: (theme) => theme.palette.divider }} />
|
||||||
{proxyContextValue.proxies?.map((proxy) => (
|
{proxyContextValue.proxies?.map((proxy) => (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -335,6 +369,9 @@ const ProxyMenu: FC<{ proxyContextValue: ProxyContextValue }> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
displayInitial: {
|
||||||
|
display: "initial",
|
||||||
|
},
|
||||||
root: {
|
root: {
|
||||||
height: navHeight,
|
height: navHeight,
|
||||||
background: theme.palette.background.paper,
|
background: theme.palette.background.paper,
|
||||||
|
@ -3,7 +3,7 @@ import { AvatarData } from "components/AvatarData/AvatarData"
|
|||||||
import { Avatar } from "components/Avatar/Avatar"
|
import { Avatar } from "components/Avatar/Avatar"
|
||||||
import TableCell from "@mui/material/TableCell"
|
import TableCell from "@mui/material/TableCell"
|
||||||
import TableRow from "@mui/material/TableRow"
|
import TableRow from "@mui/material/TableRow"
|
||||||
import { FC } from "react"
|
import { FC, useState } from "react"
|
||||||
import {
|
import {
|
||||||
HealthyBadge,
|
HealthyBadge,
|
||||||
NotHealthyBadge,
|
NotHealthyBadge,
|
||||||
@ -12,22 +12,51 @@ import {
|
|||||||
} from "components/DeploySettingsLayout/Badges"
|
} from "components/DeploySettingsLayout/Badges"
|
||||||
import { ProxyLatencyReport } from "contexts/useProxyLatency"
|
import { ProxyLatencyReport } from "contexts/useProxyLatency"
|
||||||
import { getLatencyColor } from "utils/latency"
|
import { getLatencyColor } from "utils/latency"
|
||||||
|
import Collapse from "@mui/material/Collapse"
|
||||||
|
import { makeStyles } from "@mui/styles"
|
||||||
|
import { combineClasses } from "utils/combineClasses"
|
||||||
|
import ListItem from "@mui/material/ListItem"
|
||||||
|
import List from "@mui/material/List"
|
||||||
|
import ListSubheader from "@mui/material/ListSubheader"
|
||||||
|
import { Maybe } from "components/Conditionals/Maybe"
|
||||||
|
import { CodeExample } from "components/CodeExample/CodeExample"
|
||||||
|
|
||||||
export const ProxyRow: FC<{
|
export const ProxyRow: FC<{
|
||||||
latency?: ProxyLatencyReport
|
latency?: ProxyLatencyReport
|
||||||
proxy: Region
|
proxy: Region
|
||||||
}> = ({ proxy, latency }) => {
|
}> = ({ proxy, latency }) => {
|
||||||
|
const styles = useStyles()
|
||||||
// If we have a more specific proxy status, use that.
|
// If we have a more specific proxy status, use that.
|
||||||
// All users can see healthy/unhealthy, some can see more.
|
// All users can see healthy/unhealthy, some can see more.
|
||||||
let statusBadge = <ProxyStatus proxy={proxy} />
|
let statusBadge = <ProxyStatus proxy={proxy} />
|
||||||
|
let shouldShowMessages = false
|
||||||
if ("status" in proxy) {
|
if ("status" in proxy) {
|
||||||
statusBadge = <DetailedProxyStatus proxy={proxy as WorkspaceProxy} />
|
const wsproxy = proxy as WorkspaceProxy
|
||||||
|
statusBadge = <DetailedProxyStatus proxy={wsproxy} />
|
||||||
|
shouldShowMessages = Boolean(
|
||||||
|
(wsproxy.status?.report?.warnings &&
|
||||||
|
wsproxy.status?.report?.warnings.length > 0) ||
|
||||||
|
(wsproxy.status?.report?.errors &&
|
||||||
|
wsproxy.status?.report?.errors.length > 0),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [isMsgsOpen, setIsMsgsOpen] = useState(false)
|
||||||
|
const toggle = () => {
|
||||||
|
if (shouldShowMessages) {
|
||||||
|
setIsMsgsOpen((v) => !v)
|
||||||
|
}
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TableRow key={proxy.name} data-testid={`${proxy.name}`}>
|
<TableRow
|
||||||
<TableCell>
|
className={combineClasses({
|
||||||
|
[styles.clickable]: shouldShowMessages,
|
||||||
|
})}
|
||||||
|
key={proxy.name}
|
||||||
|
data-testid={`${proxy.name}`}
|
||||||
|
>
|
||||||
|
<TableCell onClick={toggle}>
|
||||||
<AvatarData
|
<AvatarData
|
||||||
title={
|
title={
|
||||||
proxy.display_name && proxy.display_name.length > 0
|
proxy.display_name && proxy.display_name.length > 0
|
||||||
@ -44,6 +73,7 @@ export const ProxyRow: FC<{
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
subtitle={shouldShowMessages ? "Click to view details" : undefined}
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
@ -62,10 +92,60 @@ export const ProxyRow: FC<{
|
|||||||
{latency ? `${latency.latencyMS.toFixed(0)} ms` : "Not available"}
|
{latency ? `${latency.latencyMS.toFixed(0)} ms` : "Not available"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
<Maybe condition={shouldShowMessages}>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={4} sx={{ padding: "0px !important" }}>
|
||||||
|
<Collapse in={isMsgsOpen}>
|
||||||
|
<ProxyMessagesRow proxy={proxy as WorkspaceProxy} />
|
||||||
|
</Collapse>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</Maybe>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ProxyMessagesRow: FC<{
|
||||||
|
proxy: WorkspaceProxy
|
||||||
|
}> = ({ proxy }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ProxyMessagesList
|
||||||
|
title="Errors"
|
||||||
|
messages={proxy.status?.report?.errors}
|
||||||
|
/>
|
||||||
|
<ProxyMessagesList
|
||||||
|
title="Warnings"
|
||||||
|
messages={proxy.status?.report?.warnings}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProxyMessagesList: FC<{
|
||||||
|
title: string
|
||||||
|
messages?: string[]
|
||||||
|
}> = ({ title, messages }) => {
|
||||||
|
if (!messages) {
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<List
|
||||||
|
subheader={
|
||||||
|
<ListSubheader component="div" id="nested-list-subheader">
|
||||||
|
{title}
|
||||||
|
</ListSubheader>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{messages.map((error, index) => (
|
||||||
|
<ListItem key={"message" + index}>
|
||||||
|
<CodeExample code={error} />
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// DetailedProxyStatus allows a more precise status to be displayed.
|
// DetailedProxyStatus allows a more precise status to be displayed.
|
||||||
const DetailedProxyStatus: FC<{
|
const DetailedProxyStatus: FC<{
|
||||||
proxy: WorkspaceProxy
|
proxy: WorkspaceProxy
|
||||||
@ -100,3 +180,13 @@ const ProxyStatus: FC<{
|
|||||||
|
|
||||||
return icon
|
return icon
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
clickable: {
|
||||||
|
cursor: "pointer",
|
||||||
|
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: theme.palette.action.hover,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
Reference in New Issue
Block a user