mirror of
https://github.com/coder/coder.git
synced 2026-09-22 05:05:20 +08:00
chore: update workspaces top bar to display org name (#14596)
* chore: move schedule controls to the right side of the screen * chore: add org display to workspace topbar * fix: force organizations to be readonly array * fix update type mismatch for organizations again * fix: update quota querying logic to use new endpoint * fix: add logic for handling long workspace or org names * chore: add links for workspaces by org * chore: expand tooltip styling for org * chore: expand tooltip styling for owner * refactor: split off breadcrumbs for readability * fix: display correct template version name in dropdown * fix: update overflow styling for breadcrumb segments * fix: favor org display name * fix: centralize org display name logic * fix: ensure that mock query cache key and component key are properly synced for storybook
This commit is contained in:
+3
-1
@@ -1684,11 +1684,13 @@ class ApiMethods {
|
||||
};
|
||||
|
||||
getWorkspaceQuota = async (
|
||||
organizationName: string,
|
||||
username: string,
|
||||
): Promise<TypesGen.WorkspaceQuota> => {
|
||||
const response = await this.axios.get(
|
||||
`/api/v2/workspace-quota/${encodeURIComponent(username)}`,
|
||||
`/api/v2/organizations/${encodeURIComponent(organizationName)}/members/${encodeURIComponent(username)}/workspace-quota`,
|
||||
);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { API } from "api/api";
|
||||
|
||||
export const getWorkspaceQuotaQueryKey = (username: string) => [
|
||||
username,
|
||||
"workspaceQuota",
|
||||
];
|
||||
export const getWorkspaceQuotaQueryKey = (
|
||||
organizationName: string,
|
||||
username: string,
|
||||
) => {
|
||||
return ["workspaceQuota", organizationName, username];
|
||||
};
|
||||
|
||||
export const workspaceQuota = (username: string) => {
|
||||
export const workspaceQuota = (organizationName: string, username: string) => {
|
||||
return {
|
||||
queryKey: getWorkspaceQuotaQueryKey(username),
|
||||
queryFn: () => API.getWorkspaceQuota(username),
|
||||
queryKey: getWorkspaceQuotaQueryKey(organizationName, username),
|
||||
queryFn: () => API.getWorkspaceQuota(organizationName, username),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ export interface DashboardValue {
|
||||
entitlements: Entitlements;
|
||||
experiments: Experiments;
|
||||
appearance: AppearanceConfig;
|
||||
organizations: Organization[];
|
||||
organizations: readonly Organization[];
|
||||
showOrganizations: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -99,5 +99,8 @@ export const GroupsPage: FC = () => {
|
||||
|
||||
export default GroupsPage;
|
||||
|
||||
export const getOrganizationNameByDefault = (organizations: Organization[]) =>
|
||||
organizations.find((org) => org.is_default)?.name;
|
||||
export const getOrganizationNameByDefault = (
|
||||
organizations: readonly Organization[],
|
||||
) => {
|
||||
return organizations.find((org) => org.is_default)?.name;
|
||||
};
|
||||
|
||||
@@ -12,9 +12,9 @@ import { Outlet } from "react-router-dom";
|
||||
import { DeploySettingsContext } from "../DeploySettingsPage/DeploySettingsLayout";
|
||||
import { Sidebar } from "./Sidebar";
|
||||
|
||||
type OrganizationSettingsValue = {
|
||||
organizations: Organization[];
|
||||
};
|
||||
type OrganizationSettingsValue = Readonly<{
|
||||
organizations: readonly Organization[];
|
||||
}>;
|
||||
|
||||
export const useOrganizationSettings = (): OrganizationSettingsValue => {
|
||||
const { organizations } = useDashboard();
|
||||
|
||||
@@ -59,5 +59,9 @@ const OrganizationProvisionersPage: FC = () => {
|
||||
|
||||
export default OrganizationProvisionersPage;
|
||||
|
||||
const getOrganizationByName = (organizations: Organization[], name: string) =>
|
||||
organizations.find((org) => org.name === name);
|
||||
const getOrganizationByName = (
|
||||
organizations: readonly Organization[],
|
||||
name: string,
|
||||
) => {
|
||||
return organizations.find((org) => org.name === name);
|
||||
};
|
||||
|
||||
@@ -55,7 +55,7 @@ const OrganizationSettingsPage: FC = () => {
|
||||
// Redirect /organizations => /organizations/default-org, or if they cannot edit
|
||||
// the default org, then the first org they can edit, if any.
|
||||
if (!organizationName) {
|
||||
const editableOrg = organizations
|
||||
const editableOrg = [...organizations]
|
||||
.sort((a, b) => {
|
||||
// Prefer default org (it may not be first).
|
||||
// JavaScript will happily subtract booleans, but use numbers to keep
|
||||
@@ -112,5 +112,9 @@ const OrganizationSettingsPage: FC = () => {
|
||||
|
||||
export default OrganizationSettingsPage;
|
||||
|
||||
const getOrganizationByName = (organizations: Organization[], name: string) =>
|
||||
organizations.find((org) => org.name === name);
|
||||
const getOrganizationByName = (
|
||||
organizations: readonly Organization[],
|
||||
name: string,
|
||||
) => {
|
||||
return organizations.find((org) => org.name === name);
|
||||
};
|
||||
|
||||
@@ -45,8 +45,11 @@ export const TemplateRedirectController: FC = () => {
|
||||
return <Outlet />;
|
||||
};
|
||||
|
||||
const getOrganizationNameByDefault = (organizations: Organization[]) =>
|
||||
organizations.find((org) => org.is_default)?.name;
|
||||
const getOrganizationNameByDefault = (
|
||||
organizations: readonly Organization[],
|
||||
) => {
|
||||
return organizations.find((org) => org.is_default)?.name;
|
||||
};
|
||||
|
||||
// I really hate doing it this way, but React Router does not provide a better way.
|
||||
const removePrefix = (self: string, prefix: string) =>
|
||||
|
||||
@@ -220,6 +220,12 @@ export const WorkspaceNotifications: FC<WorkspaceNotificationsProps> = ({
|
||||
(n) => n.severity === "warning",
|
||||
);
|
||||
|
||||
// We have to avoid rendering out a div at all if there is no content so
|
||||
// that we don't introduce additional gaps via the parent flex container
|
||||
if (infoNotifications.length === 0 && warningNotifications.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div css={styles.notificationsGroup}>
|
||||
{infoNotifications.length > 0 && (
|
||||
|
||||
@@ -30,14 +30,15 @@ import {
|
||||
} from "utils/schedule";
|
||||
import { isWorkspaceOn } from "utils/workspace";
|
||||
|
||||
export interface WorkspaceScheduleContainerProps {
|
||||
interface WorkspaceScheduleContainerProps {
|
||||
children?: ReactNode;
|
||||
onClickIcon?: () => void;
|
||||
}
|
||||
|
||||
export const WorkspaceScheduleContainer: FC<
|
||||
WorkspaceScheduleContainerProps
|
||||
> = ({ children, onClickIcon }) => {
|
||||
const WorkspaceScheduleContainer: FC<WorkspaceScheduleContainerProps> = ({
|
||||
children,
|
||||
onClickIcon,
|
||||
}) => {
|
||||
const icon = (
|
||||
<TopbarIcon>
|
||||
<ScheduleOutlined aria-label="Schedule" />
|
||||
@@ -49,6 +50,7 @@ export const WorkspaceScheduleContainer: FC<
|
||||
<Tooltip title="Schedule">
|
||||
{onClickIcon ? (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="schedule-icon-button"
|
||||
onClick={onClickIcon}
|
||||
css={styles.scheduleIconButton}
|
||||
@@ -294,6 +296,7 @@ const styles = {
|
||||
padding: 0,
|
||||
fontSize: "inherit",
|
||||
lineHeight: "inherit",
|
||||
cursor: "pointer",
|
||||
},
|
||||
|
||||
scheduleValue: {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { expect, screen, userEvent, waitFor, within } from "@storybook/test";
|
||||
import { getWorkspaceQuotaQueryKey } from "api/queries/workspaceQuota";
|
||||
import type { Workspace, WorkspaceQuota } from "api/typesGenerated";
|
||||
import { addHours, addMinutes } from "date-fns";
|
||||
import {
|
||||
MockOrganization,
|
||||
MockTemplate,
|
||||
MockTemplateVersion,
|
||||
MockUser,
|
||||
@@ -11,9 +13,12 @@ import {
|
||||
import { withDashboardProvider } from "testHelpers/storybook";
|
||||
import { WorkspaceTopbar } from "./WorkspaceTopbar";
|
||||
|
||||
// We want a workspace without a deadline to not pollute the screenshot
|
||||
const baseWorkspace = {
|
||||
// We want a workspace without a deadline to not pollute the screenshot. Also
|
||||
// want to make sure that the workspace is synced to our other mock values
|
||||
const baseWorkspace: Workspace = {
|
||||
...MockWorkspace,
|
||||
organization_name: MockOrganization.name,
|
||||
organization_id: MockOrganization.id,
|
||||
latest_build: {
|
||||
...MockWorkspace.latest_build,
|
||||
deadline: undefined,
|
||||
@@ -262,15 +267,37 @@ export const WithFarAwayDeadlineRequiredByTemplate: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const WithQuota: Story = {
|
||||
export const WithQuotaNoOrgs: Story = {
|
||||
parameters: {
|
||||
showOrganizations: false,
|
||||
queries: [
|
||||
{
|
||||
key: getWorkspaceQuotaQueryKey(MockUser.username),
|
||||
key: getWorkspaceQuotaQueryKey(
|
||||
MockOrganization.name,
|
||||
MockUser.username,
|
||||
),
|
||||
data: {
|
||||
credits_consumed: 2,
|
||||
budget: 40,
|
||||
},
|
||||
} satisfies WorkspaceQuota,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const WithQuotaWithOrgs: Story = {
|
||||
parameters: {
|
||||
showOrganizations: true,
|
||||
queries: [
|
||||
{
|
||||
key: getWorkspaceQuotaQueryKey(
|
||||
MockOrganization.name,
|
||||
MockUser.username,
|
||||
),
|
||||
data: {
|
||||
credits_consumed: 2,
|
||||
budget: 40,
|
||||
} satisfies WorkspaceQuota,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
|
||||
import ArrowBackOutlined from "@mui/icons-material/ArrowBackOutlined";
|
||||
import DeleteOutline from "@mui/icons-material/DeleteOutline";
|
||||
import MonetizationOnOutlined from "@mui/icons-material/MonetizationOnOutlined";
|
||||
import QuotaIcon from "@mui/icons-material/MonetizationOnOutlined";
|
||||
import Link from "@mui/material/Link";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import { workspaceQuota } from "api/queries/workspaceQuota";
|
||||
@@ -87,18 +87,22 @@ export const WorkspaceTopbar: FC<WorkspaceProps> = ({
|
||||
latestVersion,
|
||||
permissions,
|
||||
}) => {
|
||||
const { entitlements, organizations, showOrganizations } = useDashboard();
|
||||
const getLink = useLinks();
|
||||
const theme = useTheme();
|
||||
|
||||
// Quota
|
||||
const hasDailyCost = workspace.latest_build.daily_cost > 0;
|
||||
const { data: quota } = useQuery({
|
||||
...workspaceQuota(workspace.owner_name),
|
||||
...workspaceQuota(workspace.organization_name, workspace.owner_name),
|
||||
|
||||
// Don't need to tie the enabled condition to showOrganizations because
|
||||
// even if the customer hasn't enabled the orgs enterprise feature, all
|
||||
// workspaces have an associated organization under the hood
|
||||
enabled: hasDailyCost,
|
||||
});
|
||||
|
||||
// Dormant
|
||||
const { entitlements } = useDashboard();
|
||||
const allowAdvancedScheduling =
|
||||
entitlements.features.advanced_template_scheduling.enabled;
|
||||
// This check can be removed when https://github.com/coder/coder/milestone/19
|
||||
@@ -108,6 +112,12 @@ export const WorkspaceTopbar: FC<WorkspaceProps> = ({
|
||||
allowAdvancedScheduling,
|
||||
);
|
||||
|
||||
const activeOrg = organizations.find(
|
||||
(org) => org.id === workspace.organization_id,
|
||||
);
|
||||
|
||||
const orgDisplayName = activeOrg?.display_name || workspace.organization_name;
|
||||
|
||||
const isImmutable =
|
||||
workspace.latest_build.status === "deleted" ||
|
||||
workspace.latest_build.status === "deleting";
|
||||
@@ -124,90 +134,71 @@ export const WorkspaceTopbar: FC<WorkspaceProps> = ({
|
||||
</TopbarIconButton>
|
||||
</Tooltip>
|
||||
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
columnGap: 24,
|
||||
rowGap: 8,
|
||||
flexWrap: "wrap",
|
||||
// 12px - It is needed to keep vertical spacing when the content is wrapped
|
||||
padding: "12px 0 12px 16px",
|
||||
}}
|
||||
>
|
||||
<div css={styles.topbarLeft}>
|
||||
<TopbarData>
|
||||
<UserAvatar
|
||||
size="xs"
|
||||
username={workspace.owner_name}
|
||||
avatarURL={workspace.owner_avatar_url}
|
||||
<OwnerBreadcrumb
|
||||
ownerName={workspace.owner_name}
|
||||
ownerAvatarUrl={workspace.owner_avatar_url}
|
||||
/>
|
||||
<Tooltip title="Owner">
|
||||
<span>{workspace.owner_name}</span>
|
||||
</Tooltip>
|
||||
<TopbarDivider />
|
||||
<Popover mode="hover">
|
||||
<PopoverTrigger>
|
||||
<span
|
||||
css={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
cursor: "default",
|
||||
padding: "4px 0",
|
||||
}}
|
||||
>
|
||||
<TopbarAvatar src={workspace.template_icon} />
|
||||
<span css={{ fontWeight: 500 }}>{workspace.name}</span>
|
||||
</span>
|
||||
</PopoverTrigger>
|
||||
|
||||
<HelpTooltipContent
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
|
||||
transformOrigin={{ vertical: "top", horizontal: "center" }}
|
||||
>
|
||||
<AvatarData
|
||||
title={
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to={templateLink}
|
||||
css={{ color: "inherit" }}
|
||||
>
|
||||
{workspace.template_display_name.length > 0
|
||||
? workspace.template_display_name
|
||||
: workspace.template_name}
|
||||
</Link>
|
||||
}
|
||||
subtitle={
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to={`${templateLink}/versions/${workspace.latest_build.template_version_name}`}
|
||||
css={{ color: "inherit" }}
|
||||
>
|
||||
{workspace.latest_build.template_version_name}
|
||||
</Link>
|
||||
}
|
||||
avatar={
|
||||
workspace.template_icon !== "" && (
|
||||
<ExternalAvatar
|
||||
src={workspace.template_icon}
|
||||
variant="square"
|
||||
fitImage
|
||||
/>
|
||||
)
|
||||
{showOrganizations && (
|
||||
<>
|
||||
<TopbarDivider />
|
||||
<OrganizationBreadcrumb
|
||||
orgName={orgDisplayName}
|
||||
orgIconUrl={activeOrg?.icon}
|
||||
orgPageUrl={
|
||||
showOrganizations
|
||||
? `/organizations/${encodeURIComponent(workspace.organization_name)}`
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</HelpTooltipContent>
|
||||
</Popover>
|
||||
</TopbarData>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isImmutable && (
|
||||
<WorkspaceScheduleControls
|
||||
workspace={workspace}
|
||||
template={template}
|
||||
canUpdateSchedule={
|
||||
canUpdateWorkspace && template.allow_user_autostop
|
||||
<TopbarDivider />
|
||||
|
||||
<WorkspaceBreadcrumb
|
||||
workspaceName={workspace.name}
|
||||
templateIconUrl={workspace.template_icon}
|
||||
rootTemplateUrl={templateLink}
|
||||
templateVersionName={workspace.template_name}
|
||||
templateVersionDisplayName={workspace.template_display_name}
|
||||
latestBuildVersionName={
|
||||
workspace.latest_build.template_version_name
|
||||
}
|
||||
/>
|
||||
</TopbarData>
|
||||
|
||||
{quota && quota.budget > 0 && (
|
||||
<Link
|
||||
component={RouterLink}
|
||||
css={{ color: "inherit" }}
|
||||
to={
|
||||
showOrganizations
|
||||
? `/workspaces?filter=organization:${encodeURIComponent(workspace.organization_name)}`
|
||||
: "/workspaces"
|
||||
}
|
||||
title={
|
||||
showOrganizations
|
||||
? `See affected workspaces for ${orgDisplayName}`
|
||||
: "See affected workspaces"
|
||||
}
|
||||
>
|
||||
<TopbarData>
|
||||
<TopbarIcon>
|
||||
<QuotaIcon aria-label="Daily usage" />
|
||||
</TopbarIcon>
|
||||
|
||||
<span>
|
||||
{workspace.latest_build.daily_cost}{" "}
|
||||
<span css={{ color: theme.palette.text.secondary }}>
|
||||
credits of
|
||||
</span>{" "}
|
||||
{quota.budget}
|
||||
</span>
|
||||
</TopbarData>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{shouldDisplayDormantData && (
|
||||
@@ -221,72 +212,238 @@ export const WorkspaceTopbar: FC<WorkspaceProps> = ({
|
||||
title="Schedule settings"
|
||||
css={{ color: "inherit" }}
|
||||
>
|
||||
Deletion on {new Date(workspace.deleting_at!).toLocaleString()}
|
||||
{workspace.deleting_at ? (
|
||||
<>
|
||||
Deletion on {new Date(workspace.deleting_at).toLocaleString()}
|
||||
</>
|
||||
) : (
|
||||
"Deletion soon"
|
||||
)}
|
||||
</Link>
|
||||
</TopbarData>
|
||||
)}
|
||||
|
||||
{quota && quota.budget > 0 && (
|
||||
<TopbarData>
|
||||
<TopbarIcon>
|
||||
<Tooltip title="Daily usage">
|
||||
<MonetizationOnOutlined aria-label="Daily usage" />
|
||||
</Tooltip>
|
||||
</TopbarIcon>
|
||||
<span>
|
||||
{workspace.latest_build.daily_cost}{" "}
|
||||
<span css={{ color: theme.palette.text.secondary }}>
|
||||
credits of
|
||||
</span>{" "}
|
||||
{quota.budget}
|
||||
</span>
|
||||
</TopbarData>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
css={{
|
||||
marginLeft: "auto",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
{!isImmutable && (
|
||||
<>
|
||||
<WorkspaceNotifications
|
||||
workspace={workspace}
|
||||
template={template}
|
||||
latestVersion={latestVersion}
|
||||
permissions={permissions}
|
||||
onRestartWorkspace={handleRestart}
|
||||
onUpdateWorkspace={handleUpdate}
|
||||
onActivateWorkspace={handleDormantActivate}
|
||||
/>
|
||||
<WorkspaceStatusBadge workspace={workspace} />
|
||||
<WorkspaceActions
|
||||
workspace={workspace}
|
||||
handleStart={handleStart}
|
||||
handleStop={handleStop}
|
||||
handleRestart={handleRestart}
|
||||
handleDelete={handleDelete}
|
||||
handleUpdate={handleUpdate}
|
||||
handleCancel={handleCancel}
|
||||
handleSettings={handleSettings}
|
||||
handleRetry={handleRetry}
|
||||
handleDebug={handleDebug}
|
||||
handleChangeVersion={handleChangeVersion}
|
||||
handleDormantActivate={handleDormantActivate}
|
||||
handleToggleFavorite={handleToggleFavorite}
|
||||
canDebug={canDebugMode}
|
||||
canChangeVersions={canChangeVersions}
|
||||
isUpdating={isUpdating}
|
||||
isRestarting={isRestarting}
|
||||
isOwner={isOwner}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{!isImmutable && (
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<WorkspaceScheduleControls
|
||||
workspace={workspace}
|
||||
template={template}
|
||||
canUpdateSchedule={
|
||||
canUpdateWorkspace && template.allow_user_autostop
|
||||
}
|
||||
/>
|
||||
<WorkspaceNotifications
|
||||
workspace={workspace}
|
||||
template={template}
|
||||
latestVersion={latestVersion}
|
||||
permissions={permissions}
|
||||
onRestartWorkspace={handleRestart}
|
||||
onUpdateWorkspace={handleUpdate}
|
||||
onActivateWorkspace={handleDormantActivate}
|
||||
/>
|
||||
<WorkspaceStatusBadge workspace={workspace} />
|
||||
<WorkspaceActions
|
||||
workspace={workspace}
|
||||
handleStart={handleStart}
|
||||
handleStop={handleStop}
|
||||
handleRestart={handleRestart}
|
||||
handleDelete={handleDelete}
|
||||
handleUpdate={handleUpdate}
|
||||
handleCancel={handleCancel}
|
||||
handleSettings={handleSettings}
|
||||
handleRetry={handleRetry}
|
||||
handleDebug={handleDebug}
|
||||
handleChangeVersion={handleChangeVersion}
|
||||
handleDormantActivate={handleDormantActivate}
|
||||
handleToggleFavorite={handleToggleFavorite}
|
||||
canDebug={canDebugMode}
|
||||
canChangeVersions={canChangeVersions}
|
||||
isUpdating={isUpdating}
|
||||
isRestarting={isRestarting}
|
||||
isOwner={isOwner}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Topbar>
|
||||
);
|
||||
};
|
||||
|
||||
type OwnerBreadcrumbProps = Readonly<{
|
||||
ownerName: string;
|
||||
ownerAvatarUrl: string;
|
||||
}>;
|
||||
|
||||
const OwnerBreadcrumb: FC<OwnerBreadcrumbProps> = ({
|
||||
ownerName,
|
||||
ownerAvatarUrl,
|
||||
}) => {
|
||||
return (
|
||||
<Popover mode="hover">
|
||||
<PopoverTrigger>
|
||||
<span css={styles.breadcrumbSegment}>
|
||||
<UserAvatar
|
||||
size="xs"
|
||||
username={ownerName}
|
||||
avatarURL={ownerAvatarUrl}
|
||||
/>
|
||||
<span css={styles.breadcrumbText}>{ownerName}</span>
|
||||
</span>
|
||||
</PopoverTrigger>
|
||||
|
||||
<HelpTooltipContent
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
|
||||
transformOrigin={{ vertical: "top", horizontal: "center" }}
|
||||
>
|
||||
<AvatarData
|
||||
title={ownerName}
|
||||
subtitle="Owner"
|
||||
avatar={ownerAvatarUrl}
|
||||
/>
|
||||
</HelpTooltipContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
type OrganizationBreadcrumbProps = Readonly<{
|
||||
orgName: string;
|
||||
orgPageUrl?: string;
|
||||
orgIconUrl?: string;
|
||||
}>;
|
||||
|
||||
const OrganizationBreadcrumb: FC<OrganizationBreadcrumbProps> = ({
|
||||
orgName,
|
||||
orgPageUrl,
|
||||
orgIconUrl,
|
||||
}) => {
|
||||
return (
|
||||
<Popover mode="hover">
|
||||
<PopoverTrigger>
|
||||
<span css={styles.breadcrumbSegment}>
|
||||
<UserAvatar size="xs" src={orgIconUrl ?? ""} username={orgName} />
|
||||
<span css={styles.breadcrumbText}>{orgName}</span>
|
||||
</span>
|
||||
</PopoverTrigger>
|
||||
|
||||
<HelpTooltipContent
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
|
||||
transformOrigin={{ vertical: "top", horizontal: "center" }}
|
||||
>
|
||||
<AvatarData
|
||||
title={
|
||||
orgPageUrl ? (
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to={orgPageUrl}
|
||||
css={{ color: "inherit" }}
|
||||
>
|
||||
{orgName}
|
||||
</Link>
|
||||
) : (
|
||||
orgName
|
||||
)
|
||||
}
|
||||
subtitle="Organization"
|
||||
avatar={
|
||||
orgIconUrl && (
|
||||
<ExternalAvatar src={orgIconUrl} variant="square" fitImage />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</HelpTooltipContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
type WorkspaceBreadcrumbProps = Readonly<{
|
||||
workspaceName: string;
|
||||
templateIconUrl: string;
|
||||
rootTemplateUrl: string;
|
||||
templateVersionName: string;
|
||||
latestBuildVersionName: string;
|
||||
templateVersionDisplayName?: string;
|
||||
}>;
|
||||
|
||||
const WorkspaceBreadcrumb: FC<WorkspaceBreadcrumbProps> = ({
|
||||
workspaceName,
|
||||
templateIconUrl,
|
||||
rootTemplateUrl,
|
||||
templateVersionName,
|
||||
latestBuildVersionName,
|
||||
templateVersionDisplayName = templateVersionName,
|
||||
}) => {
|
||||
return (
|
||||
<Popover mode="hover">
|
||||
<PopoverTrigger>
|
||||
<span css={styles.breadcrumbSegment}>
|
||||
<TopbarAvatar src={templateIconUrl} />
|
||||
<span css={[styles.breadcrumbText, { fontWeight: 500 }]}>
|
||||
{workspaceName}
|
||||
</span>
|
||||
</span>
|
||||
</PopoverTrigger>
|
||||
|
||||
<HelpTooltipContent
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
|
||||
transformOrigin={{ vertical: "top", horizontal: "center" }}
|
||||
>
|
||||
<AvatarData
|
||||
title={
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to={rootTemplateUrl}
|
||||
css={{ color: "inherit" }}
|
||||
>
|
||||
{templateVersionDisplayName}
|
||||
</Link>
|
||||
}
|
||||
subtitle={
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to={`${rootTemplateUrl}/versions/${encodeURIComponent(templateVersionName)}`}
|
||||
css={{ color: "inherit" }}
|
||||
>
|
||||
Version: {latestBuildVersionName}
|
||||
</Link>
|
||||
}
|
||||
avatar={
|
||||
<ExternalAvatar src={templateIconUrl} variant="square" fitImage />
|
||||
}
|
||||
/>
|
||||
</HelpTooltipContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
topbarLeft: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
columnGap: 24,
|
||||
rowGap: 8,
|
||||
flexWrap: "wrap",
|
||||
// 12px - It is needed to keep vertical spacing when the content is wrapped
|
||||
padding: "12px",
|
||||
marginRight: "auto",
|
||||
},
|
||||
|
||||
breadcrumbSegment: {
|
||||
display: "flex",
|
||||
flexFlow: "row nowrap",
|
||||
gap: "8px",
|
||||
maxWidth: "160px",
|
||||
whiteSpace: "nowrap",
|
||||
cursor: "default",
|
||||
},
|
||||
|
||||
breadcrumbText: {
|
||||
overflowX: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
},
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
Reference in New Issue
Block a user