mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: Show custom resource icons in the UI (#4020)
This commit is contained in:
@@ -637,6 +637,7 @@ export interface WorkspaceResource {
|
||||
readonly type: string
|
||||
readonly name: string
|
||||
readonly hide: boolean
|
||||
readonly icon: string
|
||||
readonly agents?: WorkspaceAgent[]
|
||||
readonly metadata?: WorkspaceResourceMetadata[]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Story } from "@storybook/react"
|
||||
import { MockWorkspaceResource } from "testHelpers/entities"
|
||||
import { ResourceAvatar, ResourceAvatarProps } from "./ResourceAvatar"
|
||||
|
||||
export default {
|
||||
@@ -10,25 +11,40 @@ const Template: Story<ResourceAvatarProps> = (args) => <ResourceAvatar {...args}
|
||||
|
||||
export const VolumeResource = Template.bind({})
|
||||
VolumeResource.args = {
|
||||
type: "docker_volume",
|
||||
resource: {
|
||||
...MockWorkspaceResource,
|
||||
type: "docker_volume",
|
||||
},
|
||||
}
|
||||
|
||||
export const ComputeResource = Template.bind({})
|
||||
ComputeResource.args = {
|
||||
type: "docker_container",
|
||||
resource: {
|
||||
...MockWorkspaceResource,
|
||||
type: "docker_container",
|
||||
},
|
||||
}
|
||||
|
||||
export const ImageResource = Template.bind({})
|
||||
ImageResource.args = {
|
||||
type: "docker_image",
|
||||
resource: {
|
||||
...MockWorkspaceResource,
|
||||
type: "docker_image",
|
||||
},
|
||||
}
|
||||
|
||||
export const NullResource = Template.bind({})
|
||||
NullResource.args = {
|
||||
type: "null_resource",
|
||||
resource: {
|
||||
...MockWorkspaceResource,
|
||||
type: "null_resource",
|
||||
},
|
||||
}
|
||||
|
||||
export const UnknownResource = Template.bind({})
|
||||
UnknownResource.args = {
|
||||
type: "noexistentvalue",
|
||||
resource: {
|
||||
...MockWorkspaceResource,
|
||||
type: "noexistentvalue",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,51 +1,47 @@
|
||||
import Avatar from "@material-ui/core/Avatar"
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import FolderIcon from "@material-ui/icons/FolderOutlined"
|
||||
import ImageIcon from "@material-ui/icons/ImageOutlined"
|
||||
import MemoryIcon from "@material-ui/icons/MemoryOutlined"
|
||||
import WidgetsIcon from "@material-ui/icons/WidgetsOutlined"
|
||||
import React from "react"
|
||||
import { WorkspaceResource } from "../../api/typesGenerated"
|
||||
|
||||
// For this special case, we need to apply a different style because how this
|
||||
// particular icon has been designed
|
||||
const AdjustedMemoryIcon: typeof MemoryIcon = ({ style, ...props }) => {
|
||||
return <MemoryIcon style={{ ...style, fontSize: 24 }} {...props} />
|
||||
}
|
||||
|
||||
// NOTE@jsjoeio, @BrunoQuaresma
|
||||
// NOTE @jsjoeio, @BrunoQuaresma
|
||||
// These resources (i.e. docker_image, kubernetes_deployment) map to Terraform
|
||||
// resource types. These are the most used ones and are based on user usage.
|
||||
// We may want to update from time-to-time.
|
||||
const iconByResource: Record<WorkspaceResource["type"], typeof MemoryIcon | undefined> = {
|
||||
docker_volume: FolderIcon,
|
||||
docker_container: AdjustedMemoryIcon,
|
||||
docker_image: ImageIcon,
|
||||
kubernetes_persistent_volume_claim: FolderIcon,
|
||||
kubernetes_pod: AdjustedMemoryIcon,
|
||||
google_compute_disk: FolderIcon,
|
||||
google_compute_instance: AdjustedMemoryIcon,
|
||||
aws_instance: AdjustedMemoryIcon,
|
||||
kubernetes_deployment: AdjustedMemoryIcon,
|
||||
null_resource: WidgetsIcon,
|
||||
const iconPathByResource: Record<WorkspaceResource["type"], string> = {
|
||||
docker_volume: "/icon/folder.svg",
|
||||
docker_container: "/icon/memory.svg",
|
||||
docker_image: "/icon/image.svg",
|
||||
kubernetes_persistent_volume_claim: "/icon/folder.svg",
|
||||
kubernetes_pod: "/icon/memory.svg",
|
||||
google_compute_disk: "/icon/folder.svg",
|
||||
google_compute_instance: "/icon/memory.svg",
|
||||
aws_instance: "/icon/memory.svg",
|
||||
kubernetes_deployment: "/icon/memory.svg",
|
||||
null_resource: "/icon/widgets.svg",
|
||||
}
|
||||
|
||||
export type ResourceAvatarProps = { type: WorkspaceResource["type"] }
|
||||
export type ResourceAvatarProps = { resource: WorkspaceResource }
|
||||
|
||||
export const ResourceAvatar: React.FC<ResourceAvatarProps> = ({ type }) => {
|
||||
const IconComponent = iconByResource[type] ?? WidgetsIcon
|
||||
export const ResourceAvatar: React.FC<ResourceAvatarProps> = ({ resource }) => {
|
||||
const hasIcon = resource.icon && resource.icon !== ""
|
||||
const avatarSrc = hasIcon
|
||||
? resource.icon
|
||||
: // resource.type is dynamic so iconPathByResource[resource.type] can be null
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
iconPathByResource[resource.type] ?? iconPathByResource["null_resource"]
|
||||
const styles = useStyles()
|
||||
|
||||
return (
|
||||
<Avatar className={styles.resourceAvatar}>
|
||||
<IconComponent style={{ fontSize: 20 }} />
|
||||
</Avatar>
|
||||
)
|
||||
return <Avatar className={styles.resourceAvatar} src={avatarSrc} />
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
resourceAvatar: {
|
||||
color: theme.palette.info.contrastText,
|
||||
backgroundColor: theme.palette.info.main,
|
||||
|
||||
"& img": {
|
||||
width: 20,
|
||||
height: 20,
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -49,7 +49,7 @@ export const ResourceAvatarData: FC<ResourceAvatarDataProps> = ({ resource }) =>
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className={styles.avatarWrapper}>
|
||||
<ResourceAvatar type={resource.type} />
|
||||
<ResourceAvatar resource={resource} />
|
||||
</div>
|
||||
|
||||
<TableCellData>
|
||||
|
||||
@@ -61,7 +61,7 @@ export const TemplateResourcesTable: FC<React.PropsWithChildren<TemplateResource
|
||||
title={resource.name}
|
||||
subtitle={resource.type}
|
||||
highlightTitle
|
||||
avatar={<ResourceAvatar type={resource.type} />}
|
||||
avatar={<ResourceAvatar resource={resource} />}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell colSpan={3}></TableCell>
|
||||
@@ -79,7 +79,7 @@ export const TemplateResourcesTable: FC<React.PropsWithChildren<TemplateResource
|
||||
title={resource.name}
|
||||
subtitle={resource.type}
|
||||
highlightTitle
|
||||
avatar={<ResourceAvatar type={resource.type} />}
|
||||
avatar={<ResourceAvatar resource={resource} />}
|
||||
/>
|
||||
</TableCell>
|
||||
)}
|
||||
|
||||
@@ -394,6 +394,7 @@ export const MockWorkspaceResource: TypesGen.WorkspaceResource = {
|
||||
type: "google_compute_disk",
|
||||
workspace_transition: "start",
|
||||
hide: false,
|
||||
icon: "",
|
||||
metadata: [
|
||||
{ key: "type", value: "a-workspace-resource", sensitive: false },
|
||||
{ key: "api_key", value: "12345678", sensitive: true },
|
||||
@@ -409,6 +410,7 @@ export const MockWorkspaceResource2: TypesGen.WorkspaceResource = {
|
||||
type: "google_compute_disk",
|
||||
workspace_transition: "start",
|
||||
hide: false,
|
||||
icon: "",
|
||||
metadata: [
|
||||
{ key: "type", value: "google_compute_disk", sensitive: false },
|
||||
{ key: "size", value: "32GB", sensitive: false },
|
||||
@@ -424,6 +426,7 @@ export const MockWorkspaceResource3: TypesGen.WorkspaceResource = {
|
||||
type: "google_compute_disk",
|
||||
workspace_transition: "start",
|
||||
hide: true,
|
||||
icon: "",
|
||||
metadata: [
|
||||
{ key: "type", value: "google_compute_disk", sensitive: false },
|
||||
{ key: "size", value: "32GB", sensitive: false },
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" fill="#FFF"><path d="M24 22q-8.05 0-13.025-2.45T6 14q0-3.15 4.975-5.575Q15.95 6 24 6t13.025 2.425Q42 10.85 42 14q0 3.1-4.975 5.55Q32.05 22 24 22Zm0 10q-7.3 0-12.65-2.2Q6 27.6 6 24.5v-5q0 1.95 1.875 3.375t4.65 2.35q2.775.925 5.9 1.35Q21.55 27 24 27q2.5 0 5.6-.425 3.1-.425 5.875-1.325 2.775-.9 4.65-2.325Q42 21.5 42 19.5v5q0 3.1-5.35 5.3Q31.3 32 24 32Zm0 10q-7.3 0-12.65-2.2Q6 37.6 6 34.5v-5q0 1.95 1.875 3.375t4.65 2.35q2.775.925 5.9 1.35Q21.55 37 24 37q2.5 0 5.6-.425 3.1-.425 5.875-1.325 2.775-.9 4.65-2.325Q42 31.5 42 29.5v5q0 3.1-5.35 5.3Q31.3 42 24 42Z"/></svg>
|
||||
|
After Width: | Height: | Size: 630 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" fill="#FFF"><path d="M7.05 40q-1.2 0-2.1-.925-.9-.925-.9-2.075V11q0-1.15.9-2.075Q5.85 8 7.05 8h14l3 3h17q1.15 0 2.075.925.925.925.925 2.075v23q0 1.15-.925 2.075Q42.2 40 41.05 40Zm0-29v26h34V14H22.8l-3-3H7.05Zm0 0v26Z"/></svg>
|
||||
|
After Width: | Height: | Size: 289 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" fill="#FFF"><path d="M9 42q-1.2 0-2.1-.9Q6 40.2 6 39V9q0-1.2.9-2.1Q7.8 6 9 6h30q1.2 0 2.1.9.9.9.9 2.1v30q0 1.2-.9 2.1-.9.9-2.1.9Zm0-3h30V9H9v30Zm2.8-4.85h24.45l-7.35-9.8-6.6 8.55-4.65-6.35ZM9 39V9v30Z"/></svg>
|
||||
|
After Width: | Height: | Size: 273 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" fill="#FFF"><path d="M18.85 29.15V18.9H29.1v10.25Zm3-3h4.25V21.9h-4.25ZM18 42v-4h-5q-1.2 0-2.1-.9-.9-.9-.9-2.1v-5H6v-3h4v-6.2H6v-3h4v-5q0-1.2.9-2.1.9-.9 2.1-.9h5V6h3v3.8h6.2V6h3v3.8h5q1.2 0 2.1.9.9.9.9 2.1v5H42v3h-3.8V27H42v3h-3.8v5q0 1.2-.9 2.1-.9.9-2.1.9h-5v4h-3v-4H21v4Zm17.2-7V12.8H13V35ZM24 24Z"/></svg>
|
||||
|
After Width: | Height: | Size: 372 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" fill="#FFF"><path d="M33.95 24.7 23.3 14.05 33.95 3.4 44.6 14.05ZM6 21.1V6.05h15.05V21.1ZM26.9 42V26.95h15.05V42ZM6 42V26.95h15.05V42Zm3-23.9h9.05V9.05H9Zm25.1 2.55 6.45-6.45-6.45-6.45-6.45 6.45ZM29.9 39h9.05v-9.05H29.9ZM9 39h9.05v-9.05H9Zm9.05-20.9Zm9.6-3.9Zm-9.6 15.75Zm11.85 0Z"/></svg>
|
||||
|
After Width: | Height: | Size: 353 B |
Reference in New Issue
Block a user