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

Add default image get to PAPI and improve Dashboard #18767

Merged
merged 13 commits into from
Sep 22, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@

import { useQuery } from "@tanstack/react-query";
import { getGitpodService } from "../../service/service";
import { GetDefaultWorkspaceImageResult } from "@gitpod/gitpod-protocol";

export const useDefaultWorkspaceImageQuery = () => {
return useQuery<string>({
queryKey: ["default-workspace-image"],
export const useDefaultWorkspaceImageQuery = (workspaceId?: string) => {
return useQuery<GetDefaultWorkspaceImageResult>({
queryKey: ["default-workspace-image", { workspaceId }],
staleTime: 1000 * 60 * 10, // 10 minute
queryFn: async () => {
const image = await getGitpodService().server.getDefaultWorkspaceImage();
return image;
return await getGitpodService().server.getDefaultWorkspaceImage({ workspaceId });
},
});
};
53 changes: 39 additions & 14 deletions components/dashboard/src/start/StartPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { useDocumentTitle } from "../hooks/use-document-title";
import gitpodIcon from "../icons/gitpod.svg";
import { gitpodHostUrl } from "../service/service";
import { VerifyModal } from "./VerifyModal";
import { useDefaultWorkspaceImageQuery } from "../data/workspaces/default-workspace-image-query";

export enum StartPhase {
Checking = 0,
Expand Down Expand Up @@ -81,6 +82,7 @@ export interface StartPageProps {
title?: string;
children?: React.ReactNode;
showLatestIdeWarning?: boolean;
workspaceId: string;
}

export interface StartWorkspaceError {
Expand All @@ -90,7 +92,7 @@ export interface StartWorkspaceError {
}

export function StartPage(props: StartPageProps) {
const { phase, error } = props;
const { phase, error, workspaceId } = props;
let title = props.title || getPhaseTitle(phase, error);
useDocumentTitle("Starting");
return (
Expand All @@ -114,19 +116,11 @@ export function StartPage(props: StartPageProps) {
)}
{error && <StartError error={error} />}
{props.children}
{props.showLatestIdeWarning && (
<Alert type="warning" className="mt-4 w-96">
This workspace is configured with the latest release (unstable) for the editor.{" "}
<a
className="gp-link"
target="_blank"
rel="noreferrer"
href={gitpodHostUrl.asPreferences().toString()}
>
Change Preferences
</a>
</Alert>
)}
<WarningView
workspaceId={workspaceId}
showLatestIdeWarning={props.showLatestIdeWarning}
error={props.error}
/>
</div>
</div>
);
Expand All @@ -139,3 +133,34 @@ function StartError(props: { error: StartWorkspaceError }) {
}
return <p className="text-base text-gitpod-red w-96">{error.message}</p>;
}

function WarningView(props: { workspaceId?: string; showLatestIdeWarning?: boolean; error?: StartWorkspaceError }) {
const { data: imageInfo } = useDefaultWorkspaceImageQuery(props.workspaceId);
let useWarning: "latestIde" | "orgImage" | undefined = props.showLatestIdeWarning ? "latestIde" : undefined;
if (props.error && props.workspaceId && imageInfo?.source === "organization") {
useWarning = "orgImage";
}
return (
<div>
{useWarning === "latestIde" && (
<Alert type="warning" className="mt-4 w-96">
This workspace is configured with the latest release (unstable) for the editor.{" "}
<a
className="gp-link"
target="_blank"
rel="noreferrer"
href={gitpodHostUrl.asPreferences().toString()}
>
Change Preferences
</a>
</Alert>
)}
{useWarning === "orgImage" && (
<Alert className="w-96 mt-4" type="warning">
<span className="font-medium">Could not use workspace image?</span> Try a different workspace image
in the yaml configuration or check the default workspace image in organization settings.
</Alert>
)}
</div>
);
}
7 changes: 4 additions & 3 deletions components/dashboard/src/start/StartWorkspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,7 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
case "running":
if (isPrebuild) {
return (
<StartPage title="Prebuild in Progress">
<StartPage title="Prebuild in Progress" workspaceId={this.props.workspaceId}>
<div className="mt-6 w-11/12 lg:w-3/5">
{/* TODO(gpl) These classes are copied around in Start-/CreateWorkspace. This should properly go somewhere central. */}
<PrebuildLogs workspaceId={this.props.workspaceId} />
Expand Down Expand Up @@ -627,7 +627,7 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
isStoppingOrStoppedPhase = true;
if (isPrebuild) {
return (
<StartPage title="Prebuild in Progress">
<StartPage title="Prebuild in Progress" workspaceId={this.props.workspaceId}>
<div className="mt-6 w-11/12 lg:w-3/5">
{/* TODO(gpl) These classes are copied around in Start-/CreateWorkspace. This should properly go somewhere central. */}
<PrebuildLogs workspaceId={this.props.workspaceId} />
Expand Down Expand Up @@ -718,6 +718,7 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
error={error}
title={title}
showLatestIdeWarning={useLatest && (isError || !isStoppingOrStoppedPhase)}
workspaceId={this.props.workspaceId}
>
{statusMessage}
</StartPage>
Expand Down Expand Up @@ -778,7 +779,7 @@ function ImageBuildView(props: ImageBuildViewProps) {
}, []);

return (
<StartPage title="Building Image" phase={props.phase}>
<StartPage title="Building Image" phase={props.phase} workspaceId={props.workspaceId}>
<Suspense fallback={<div />}>
<WorkspaceLogs logsEmitter={logsEmitter} errorMessage={props.error?.message} />
</Suspense>
Expand Down
27 changes: 14 additions & 13 deletions components/dashboard/src/teams/TeamSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ export default function TeamSettingsPage() {
function OrgSettingsForm(props: { org?: OrganizationInfo }) {
const { org } = props;
const { data: settings, isLoading } = useOrgSettingsQuery();
const { data: globalDefaultImage } = useDefaultWorkspaceImageQuery();
const { data: imageInfo } = useDefaultWorkspaceImageQuery();
const updateTeamSettings = useUpdateOrgSettingsMutation();

const [showImageEditModal, setShowImageEditModal] = useState(false);
Expand Down Expand Up @@ -229,14 +229,14 @@ function OrgSettingsForm(props: { org?: OrganizationInfo }) {
<WorkspaceImageButton
disabled={!org?.isOwner}
settings={settings}
defaultWorkspaceImage={globalDefaultImage}
defaultWorkspaceImage={imageInfo?.image}
onClick={() => setShowImageEditModal(true)}
/>

{showImageEditModal && (
<OrgDefaultWorkspaceImageModal
settings={settings}
globalDefaultImage={globalDefaultImage}
globalDefaultImage={imageInfo?.image}
onClose={() => setShowImageEditModal(false)}
/>
)}
Expand Down Expand Up @@ -276,10 +276,7 @@ function WorkspaceImageButton(props: {
const image = props.settings?.defaultWorkspaceImage ?? props.defaultWorkspaceImage ?? "";

const descList = useMemo(() => {
const arr: ReactNode[] = [];
if (!props.settings?.defaultWorkspaceImage) {
arr.push(<span className="font-medium">Default image</span>);
}
const arr: ReactNode[] = [<span>Default image</span>];
if (props.disabled) {
arr.push(
<>
Expand All @@ -288,7 +285,7 @@ function WorkspaceImageButton(props: {
);
}
return arr;
}, [props.settings, props.disabled]);
}, [props.disabled]);

const renderedDescription = useMemo(() => {
return Children.toArray(descList).reduce((acc: ReactNode[], child, index) => {
Expand All @@ -304,20 +301,24 @@ function WorkspaceImageButton(props: {
<InputField disabled={props.disabled} className="w-full max-w-lg">
<div className="flex flex-col bg-gray-50 dark:bg-gray-800 p-3 rounded-lg">
<div className="flex items-center justify-between">
<div className="flex items-center overflow-hidden h-8" title={image}>
<div className="flex-1 flex items-center overflow-hidden h-8" title={image}>
<span className="w-5 h-5 mr-1">
<Stack />
</span>
<span className="truncate font-medium text-gray-700 dark:text-gray-200">
{parseDockerImage(image).repository}
</span>
&nbsp;&middot;&nbsp;
<span className="flex-none w-16 truncate text-gray-500 dark:text-gray-400">
{parseDockerImage(image).tag}
</span>
<span className="truncate text-gray-500 dark:text-gray-400">{parseDockerImage(image).tag}</span>
mustard-mh marked this conversation as resolved.
Show resolved Hide resolved
</div>
{!props.disabled && (
<Button htmlType="button" type="transparent" className="text-blue-500" onClick={props.onClick}>
<Button
htmlType="button"
type="transparent"
spacing="compact"
className="text-blue-500 flex-none"
onClick={props.onClick}
>
Change
</Button>
)}
Expand Down
34 changes: 34 additions & 0 deletions components/gitpod-protocol/go/gitpod-service.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ type APIInterface interface {
// Organization
GetOrgSettings(ctx context.Context, orgID string) (*OrganizationSettings, error)

GetDefaultWorkspaceImage(ctx context.Context, params *GetDefaultWorkspaceImageParams) (res *GetDefaultWorkspaceImageResult, err error)

// Projects
CreateProject(ctx context.Context, options *CreateProjectOptions) (*Project, error)
DeleteProject(ctx context.Context, projectID string) error
Expand Down Expand Up @@ -242,6 +244,9 @@ const (
// FunctionGetOrgSettings is the name of the getOrgSettings function
FunctionGetOrgSettings FunctionName = "getOrgSettings"

// FunctionGetDefaultWorkspaceImage is the name of the getDefaultWorkspaceImage function
FunctionGetDefaultWorkspaceImage FunctionName = "getDefaultWorkspaceImage"

// Projects
FunctionCreateProject FunctionName = "createProject"
FunctionDeleteProject FunctionName = "deleteProject"
Expand Down Expand Up @@ -1488,6 +1493,19 @@ func (gp *APIoverJSONRPC) GetOrgSettings(ctx context.Context, orgID string) (res
return
}

func (gp *APIoverJSONRPC) GetDefaultWorkspaceImage(ctx context.Context, params *GetDefaultWorkspaceImageParams) (res *GetDefaultWorkspaceImageResult, err error) {
if gp == nil {
err = errNotConnected
return
}
var _params []interface{}

_params = append(_params, params)

err = gp.C.Call(ctx, string(FunctionGetDefaultWorkspaceImage), _params, &res)
return
}

func (gp *APIoverJSONRPC) CreateProject(ctx context.Context, options *CreateProjectOptions) (res *Project, err error) {
if gp == nil {
err = errNotConnected
Expand Down Expand Up @@ -2369,3 +2387,19 @@ type IDEClient struct {
// InstallationSteps to install the client on user machine.
InstallationSteps []string `json:"installationSteps,omitempty"`
}

type GetDefaultWorkspaceImageParams struct {
WorkspaceID string `json:"workspaceId,omitempty"`
}

type WorkspaceImageSource string

const (
WorkspaceImageSourceInstallation WorkspaceImageSource = "installation"
WorkspaceImageSourceOrganization WorkspaceImageSource = "organization"
)

type GetDefaultWorkspaceImageResult struct {
Image string `json:"image,omitempty"`
Source WorkspaceImageSource `json:"source,omitempty"`
}
15 changes: 15 additions & 0 deletions components/gitpod-protocol/go/mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 15 additions & 1 deletion components/gitpod-protocol/src/gitpod-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ export interface GitpodServer extends JsonRpcServer<GitpodClient>, AdminServer,
getOrgAuthProviders(params: GitpodServer.GetOrgAuthProviderParams): Promise<AuthProviderEntry[]>;
deleteOrgAuthProvider(params: GitpodServer.DeleteOrgAuthProviderParams): Promise<void>;

getDefaultWorkspaceImage(): Promise<string>;
getDefaultWorkspaceImage(params: GetDefaultWorkspaceImageParams): Promise<GetDefaultWorkspaceImageResult>;

// Dedicated, Dedicated, Dedicated
getOnboardingState(): Promise<GitpodServer.OnboardingState>;
Expand Down Expand Up @@ -280,6 +280,20 @@ export interface RateLimiterError {
retryAfter: number;
}

export interface GetDefaultWorkspaceImageParams {
// filter with workspaceId (actually we will find with organizationId, and it's a real time finding)
workspaceId?: string;
}

export type DefaultImageSource =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do clients really need it?

| "installation" // Source installation means the image comes from Gitpod instance install config
| "organization"; // Source organization means the image comes from Organization settings

export interface GetDefaultWorkspaceImageResult {
image: string;
source: DefaultImageSource;
}

export interface CreateProjectParams {
name: string;
/** @deprecated unused */
Expand Down
26 changes: 26 additions & 0 deletions components/public-api-server/pkg/apiv1/workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,32 @@ func (s *WorkspaceService) ListWorkspaceClasses(ctx context.Context, req *connec
), nil
}

func (s *WorkspaceService) GetDefaultWorkspaceImage(ctx context.Context, req *connect.Request[v1.GetDefaultWorkspaceImageRequest]) (*connect.Response[v1.GetDefaultWorkspaceImageResponse], error) {
conn, err := getConnection(ctx, s.connectionPool)
if err != nil {
return nil, err
}
wsImage, err := conn.GetDefaultWorkspaceImage(ctx, &protocol.GetDefaultWorkspaceImageParams{
WorkspaceID: req.Msg.GetWorkspaceId(),
})
if err != nil {
log.Extract(ctx).WithError(err).Error("Failed to get default workspace image.")
return nil, proxy.ConvertError(err)
}

source := v1.GetDefaultWorkspaceImageResponse_IMAGE_SOURCE_UNSPECIFIED
if wsImage.Source == protocol.WorkspaceImageSourceInstallation {
source = v1.GetDefaultWorkspaceImageResponse_IMAGE_SOURCE_INSTALLATION
} else if wsImage.Source == protocol.WorkspaceImageSourceOrganization {
source = v1.GetDefaultWorkspaceImageResponse_IMAGE_SOURCE_ORGANIZATION
}

return connect.NewResponse(&v1.GetDefaultWorkspaceImageResponse{
Image: wsImage.Image,
Source: source,
}), nil
}

func getLimitFromPagination(pagination *v1.Pagination) (int, error) {
const (
defaultLimit = 20
Expand Down
25 changes: 25 additions & 0 deletions components/public-api/gitpod/experimental/v1/workspaces.proto
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ service WorkspacesService {

// ListWorkspaceClasses enumerates all available workspace classes.
rpc ListWorkspaceClasses(ListWorkspaceClassesRequest) returns (ListWorkspaceClassesResponse) {}

// GetDefaultWorkspaceImage returns the default workspace image from different sources.
rpc GetDefaultWorkspaceImage(GetDefaultWorkspaceImageRequest) returns (GetDefaultWorkspaceImageResponse) {}
}

message ListWorkspacesRequest {
Expand Down Expand Up @@ -433,3 +436,25 @@ message WorkspaceClass {
// is_default indicates if this workspace class is the default one
bool is_default = 4;
}

message GetDefaultWorkspaceImageRequest {
optional string workspace_id = 1;
filiptronicek marked this conversation as resolved.
Show resolved Hide resolved
}

message GetDefaultWorkspaceImageResponse {
enum ImageSource {
IMAGE_SOURCE_UNSPECIFIED = 0;

// IMAGE_SOURCE_INSTALLATION means the image from Gitpod instance install config
IMAGE_SOURCE_INSTALLATION = 1;

// IMAGE_SOURCE_ORGANIZATION means the image from Organization settings
IMAGE_SOURCE_ORGANIZATION = 2;
}

// image is the image ref
string image = 1;

// source is the source of the image
ImageSource source = 2;
}
Loading
Loading