chore!: fix workspace apps response (#17700)

Fix WorkspaceApp response type to better reflect the schema from
https://registry.terraform.io/providers/coder/coder/latest/docs/resources/app.
This commit is contained in:
Bruno Quaresma
2025-05-07 10:05:07 -03:00
committed by GitHub
parent d146115ca0
commit 9fe5b71d31
9 changed files with 22 additions and 33 deletions

View File

@ -60,14 +60,14 @@ type WorkspaceApp struct {
ID uuid.UUID `json:"id" format:"uuid"` ID uuid.UUID `json:"id" format:"uuid"`
// URL is the address being proxied to inside the workspace. // URL is the address being proxied to inside the workspace.
// If external is specified, this will be opened on the client. // If external is specified, this will be opened on the client.
URL string `json:"url"` URL string `json:"url,omitempty"`
// External specifies whether the URL should be opened externally on // External specifies whether the URL should be opened externally on
// the client or not. // the client or not.
External bool `json:"external"` External bool `json:"external"`
// Slug is a unique identifier within the agent. // Slug is a unique identifier within the agent.
Slug string `json:"slug"` Slug string `json:"slug"`
// DisplayName is a friendly name for the app. // DisplayName is a friendly name for the app.
DisplayName string `json:"display_name"` DisplayName string `json:"display_name,omitempty"`
Command string `json:"command,omitempty"` Command string `json:"command,omitempty"`
// Icon is a relative path or external URL that specifies // Icon is a relative path or external URL that specifies
// an icon to be displayed in the dashboard. // an icon to be displayed in the dashboard.
@ -81,7 +81,7 @@ type WorkspaceApp struct {
SubdomainName string `json:"subdomain_name,omitempty"` SubdomainName string `json:"subdomain_name,omitempty"`
SharingLevel WorkspaceAppSharingLevel `json:"sharing_level" enums:"owner,authenticated,public"` SharingLevel WorkspaceAppSharingLevel `json:"sharing_level" enums:"owner,authenticated,public"`
// Healthcheck specifies the configuration for checking app health. // Healthcheck specifies the configuration for checking app health.
Healthcheck Healthcheck `json:"healthcheck"` Healthcheck Healthcheck `json:"healthcheck,omitempty"`
Health WorkspaceAppHealth `json:"health"` Health WorkspaceAppHealth `json:"health"`
Hidden bool `json:"hidden"` Hidden bool `json:"hidden"`
OpenIn WorkspaceAppOpenIn `json:"open_in"` OpenIn WorkspaceAppOpenIn `json:"open_in"`

View File

@ -3474,16 +3474,16 @@ export const WorkspaceAgentStatuses: WorkspaceAgentStatus[] = [
// From codersdk/workspaceapps.go // From codersdk/workspaceapps.go
export interface WorkspaceApp { export interface WorkspaceApp {
readonly id: string; readonly id: string;
readonly url: string; readonly url?: string;
readonly external: boolean; readonly external: boolean;
readonly slug: string; readonly slug: string;
readonly display_name: string; readonly display_name?: string;
readonly command?: string; readonly command?: string;
readonly icon?: string; readonly icon?: string;
readonly subdomain: boolean; readonly subdomain: boolean;
readonly subdomain_name?: string; readonly subdomain_name?: string;
readonly sharing_level: WorkspaceAppSharingLevel; readonly sharing_level: WorkspaceAppSharingLevel;
readonly healthcheck: Healthcheck; readonly healthcheck?: Healthcheck;
readonly health: WorkspaceAppHealth; readonly health: WorkspaceAppHealth;
readonly hidden: boolean; readonly hidden: boolean;
readonly open_in: WorkspaceAppOpenIn; readonly open_in: WorkspaceAppOpenIn;

View File

@ -150,9 +150,9 @@ describe.each<{
for (const app of props.agent.apps) { for (const app of props.agent.apps) {
if (app.hidden) { if (app.hidden) {
expect(screen.queryByText(app.display_name)).toBeNull(); expect(screen.queryByText(app.display_name as string)).toBeNull();
} else { } else {
expect(screen.getByText(app.display_name)).toBeVisible(); expect(screen.getByText(app.display_name as string)).toBeVisible();
} }
} }
}); });

View File

@ -91,8 +91,10 @@ describe("AgentRowPreviewApps", () => {
"<AgentRowPreview agent={$testName} /> displays appropriately", "<AgentRowPreview agent={$testName} /> displays appropriately",
({ workspaceAgent }) => { ({ workspaceAgent }) => {
renderComponent(<AgentRowPreview agent={workspaceAgent} />); renderComponent(<AgentRowPreview agent={workspaceAgent} />);
for (const module of workspaceAgent.apps) { for (const app of workspaceAgent.apps) {
expect(screen.getByText(module.display_name)).toBeInTheDocument(); expect(
screen.getByText(app.display_name as string),
).toBeInTheDocument();
} }
for (const app of workspaceAgent.display_apps) { for (const app of workspaceAgent.display_apps) {

View File

@ -31,7 +31,7 @@ export const AgentRowPreview: FC<AgentRowPreviewProps> = ({
> >
<Stack direction="row" alignItems="baseline"> <Stack direction="row" alignItems="baseline">
<div css={styles.agentStatusWrapper}> <div css={styles.agentStatusWrapper}>
<div css={styles.agentStatusPreview}></div> <div css={styles.agentStatusPreview} />
</div> </div>
<Stack <Stack
alignItems="baseline" alignItems="baseline"

View File

@ -43,24 +43,15 @@ export const AppLink: FC<AppLinkProps> = ({ app, workspace, agent }) => {
const appsHost = proxy.preferredWildcardHostname; const appsHost = proxy.preferredWildcardHostname;
const [fetchingSessionToken, setFetchingSessionToken] = useState(false); const [fetchingSessionToken, setFetchingSessionToken] = useState(false);
const [iconError, setIconError] = useState(false); const [iconError, setIconError] = useState(false);
const theme = useTheme(); const theme = useTheme();
const username = workspace.owner_name; const username = workspace.owner_name;
const displayName = app.display_name || app.slug;
let appSlug = app.slug;
let appDisplayName = app.display_name;
if (!appSlug) {
appSlug = appDisplayName;
}
if (!appDisplayName) {
appDisplayName = appSlug;
}
const href = createAppLinkHref( const href = createAppLinkHref(
window.location.protocol, window.location.protocol,
preferredPathBase, preferredPathBase,
appsHost, appsHost,
appSlug, app.slug,
username, username,
workspace, workspace,
agent, agent,
@ -118,7 +109,7 @@ export const AppLink: FC<AppLinkProps> = ({ app, workspace, agent }) => {
// This is an external URI like "vscode://", so // This is an external URI like "vscode://", so
// it needs to be opened with the browser protocol handler. // it needs to be opened with the browser protocol handler.
const shouldOpenAppExternally = const shouldOpenAppExternally =
app.external && !app.url.startsWith("http"); app.external && app.url?.startsWith("http");
if (shouldOpenAppExternally) { if (shouldOpenAppExternally) {
// This is a magic undocumented string that is replaced // This is a magic undocumented string that is replaced
@ -140,9 +131,7 @@ export const AppLink: FC<AppLinkProps> = ({ app, workspace, agent }) => {
// an error message will be displayed. // an error message will be displayed.
const openAppExternallyFailedTimeout = 500; const openAppExternallyFailedTimeout = 500;
const openAppExternallyFailed = setTimeout(() => { const openAppExternallyFailed = setTimeout(() => {
displayError( displayError(`${displayName} must be installed first.`);
`${app.display_name !== "" ? app.display_name : app.slug} must be installed first.`,
);
}, openAppExternallyFailedTimeout); }, openAppExternallyFailedTimeout);
window.addEventListener("blur", () => { window.addEventListener("blur", () => {
clearTimeout(openAppExternallyFailed); clearTimeout(openAppExternallyFailed);
@ -156,7 +145,7 @@ export const AppLink: FC<AppLinkProps> = ({ app, workspace, agent }) => {
case "slim-window": { case "slim-window": {
window.open( window.open(
href, href,
Language.appTitle(appDisplayName, generateRandomString(12)), Language.appTitle(displayName, generateRandomString(12)),
"width=900,height=600", "width=900,height=600",
); );
return; return;
@ -169,7 +158,7 @@ export const AppLink: FC<AppLinkProps> = ({ app, workspace, agent }) => {
}} }}
> >
{icon} {icon}
{appDisplayName} {displayName}
{canShare && <ShareIcon app={app} />} {canShare && <ShareIcon app={app} />}
</a> </a>
</AgentButton> </AgentButton>

View File

@ -124,12 +124,11 @@ export const WorkspaceAppStatus = ({
let appHref: string | undefined; let appHref: string | undefined;
if (app && agent) { if (app && agent) {
const appSlug = app.slug || app.display_name;
appHref = createAppLinkHref( appHref = createAppLinkHref(
window.location.protocol, window.location.protocol,
preferredPathBase, preferredPathBase,
appsHost, appsHost,
appSlug, app.slug,
workspace.owner_name, workspace.owner_name,
workspace, workspace,
agent, agent,

View File

@ -198,12 +198,11 @@ export const AppStatuses: FC<AppStatusesProps> = ({
const agent = agents.find((agent) => agent.id === status.agent_id); const agent = agents.find((agent) => agent.id === status.agent_id);
if (currentApp && agent) { if (currentApp && agent) {
const appSlug = currentApp.slug || currentApp.display_name;
appHref = createAppLinkHref( appHref = createAppLinkHref(
window.location.protocol, window.location.protocol,
preferredPathBase, preferredPathBase,
appsHost, appsHost,
appSlug, currentApp.slug,
workspace.owner_name, workspace.owner_name,
workspace, workspace,
agent, agent,

View File

@ -11,7 +11,7 @@ export const createAppLinkHref = (
app: TypesGen.WorkspaceApp, app: TypesGen.WorkspaceApp,
): string => { ): string => {
if (app.external) { if (app.external) {
return app.url; return app.url as string;
} }
// The backend redirects if the trailing slash isn't included, so we add it // The backend redirects if the trailing slash isn't included, so we add it