Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions components/dashboard/src/Analytics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type Event =
| "feedback_submitted"
| "workspace_class_changed"
| "privacy_policy_update_accepted"
| "coachmark_dismissed"
| "modal_dismiss"
| "ide_configuration_changed"
| "status_rendered"
Expand Down Expand Up @@ -93,6 +94,11 @@ export interface TrackPolicyUpdateClick {
success: boolean;
}

export interface TrackCoachmarkDismissed {
name: string;
success: boolean;
}

interface TrackDashboardClick {
dnt?: boolean;
path: string;
Expand All @@ -119,6 +125,7 @@ export function trackEvent(event: "dotfile_repo_changed", properties: TrackDotfi
export function trackEvent(event: "feedback_submitted", properties: TrackFeedback): void;
export function trackEvent(event: "workspace_class_changed", properties: TrackWorkspaceClassChanged): void;
export function trackEvent(event: "privacy_policy_update_accepted", properties: TrackPolicyUpdateClick): void;
export function trackEvent(event: "coachmark_dismissed", properties: TrackCoachmarkDismissed): void;
export function trackEvent(event: "modal_dismiss", properties: TrackModalDismiss): void;
export function trackEvent(event: "ide_configuration_changed", properties: TrackIDEConfigurationChanged): void;
export function trackEvent(event: "status_rendered", properties: TrackStatusRendered): void;
Expand Down
6 changes: 4 additions & 2 deletions components/dashboard/src/app/AppRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import { AppNotifications } from "../AppNotifications";
import { useHasConfigurationsAndPrebuildsEnabled } from "../data/featureflag-query";
import { projectsPathInstallGitHubApp } from "../projects/projects.routes";
import { Heading1, Subheading } from "@podkit/typography/Headings";
import { PrebuildDetailPage } from "../repositories/detail/prebuilds/PrebuildDetailPage";
import { PrebuildDetailPage } from "../prebuilds/detail/PrebuildDetailPage";

const Workspaces = React.lazy(() => import(/* webpackPrefetch: true */ "../workspaces/Workspaces"));
const Account = React.lazy(() => import(/* webpackPrefetch: true */ "../user-settings/Account"));
Expand Down Expand Up @@ -210,7 +210,9 @@ export const AppRoutes = () => {
<Route exact path={`/projects/:projectSlug/:prebuildId`} component={Prebuild} />

{configurationsAndPrebuilds && <Route exact path={`/prebuilds`} component={PrebuildListPage} />}
{configurationsAndPrebuilds && <Route path="/prebuilds/:prebuildId" component={PrebuildDetailPage} />}
{configurationsAndPrebuilds && (
<Route path="/prebuilds/:prebuildId" component={PrebuildDetailPage} />
)}
{configurationsAndPrebuilds && (
<Route exact path="/repositories" component={ConfigurationListPage} />
)}
Expand Down
34 changes: 34 additions & 0 deletions components/dashboard/src/components/podkit/popover/Popover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Copyright (c) 2024 Gitpod GmbH. All rights reserved.
* Licensed under the GNU Affero General Public License (AGPL).
* See License.AGPL.txt in the project root for license information.
*/

import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { cn } from "@podkit/lib/cn";

const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverArrow = PopoverPrimitive.Arrow;

const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-96 rounded-md border border-surface-secondary bg-pk-surface-primary py-4 px-6 text-base text-pk-content-primary shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
</PopoverPrimitive.Portal>
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;

export { Popover, PopoverTrigger, PopoverContent, PopoverArrow };
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export const useConfiguration = (configurationId: string) => {
getConfigurationQueryKey(configurationId),
async () => {
if (!configurationId) {
throw new Error("No configurationId provided");
return;
}

const { configuration } = await configurationClient.getConfiguration({
Expand Down
16 changes: 11 additions & 5 deletions components/dashboard/src/menu/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import { User, RoleOrPermission } from "@gitpod/public-api/lib/gitpod/v1/user_pb
import { getPrimaryEmail } from "@gitpod/public-api-common/lib/user-utils";
import { useHasRolePermission } from "../data/organizations/members-query";
import { OrganizationRole } from "@gitpod/public-api/lib/gitpod/v1/organization_pb";
import { ConfigurationsMigrationCoachmark } from "../repositories/coachmarks/MigrationCoachmark";
import { useFeatureFlag, useHasConfigurationsAndPrebuildsEnabled } from "../data/featureflag-query";

interface Entry {
title: string;
Expand Down Expand Up @@ -74,8 +76,10 @@ export default function Menu() {
<header className="app-container flex flex-col pt-4" data-analytics='{"button_type":"menu"}'>
<div className="flex justify-between h-10 mb-3 w-full">
<div className="flex items-center">
<OrganizationSelector />
{/* hidden on smaller screens (in it's own menu below on smaller screens) */}
<ConfigurationsMigrationCoachmark>
<OrganizationSelector />
</ConfigurationsMigrationCoachmark>
{/* hidden on smaller screens (in its own menu below on smaller screens) */}
<div className="hidden md:block pl-2">
<OrgPagesNav />
</div>
Expand Down Expand Up @@ -130,6 +134,8 @@ type OrgPagesNavProps = {
const OrgPagesNav: FC<OrgPagesNavProps> = ({ className }) => {
const location = useLocation();
const hasMemberPermission = useHasRolePermission(OrganizationRole.MEMBER);
const configurationsEnabled = useHasConfigurationsAndPrebuildsEnabled();
const prebuildsInMenu = useFeatureFlag("showPrebuildsMenuItem");

const leftMenu: Entry[] = useMemo(() => {
const menus = [
Expand All @@ -139,16 +145,16 @@ const OrgPagesNav: FC<OrgPagesNavProps> = ({ className }) => {
alternatives: ["/"],
},
];
// collaborator can't access projects
if (hasMemberPermission) {
// collaborators can't access projects
if (hasMemberPermission && (!configurationsEnabled || !prebuildsInMenu)) {
menus.push({
title: "Projects",
link: `/projects`,
alternatives: [] as string[],
});
}
return menus;
}, [hasMemberPermission]);
}, [configurationsEnabled, hasMemberPermission, prebuildsInMenu]);

return (
<div
Expand Down
4 changes: 2 additions & 2 deletions components/dashboard/src/menu/OrganizationSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default function OrganizationSelector() {
const user = useCurrentUser();
const orgs = useOrganizations();
const currentOrg = useCurrentOrg();
const members = useListOrganizationMembers().data || [];
const members = useListOrganizationMembers().data ?? [];
const owner = useIsOwner();
const hasMemberPermission = useHasRolePermission(OrganizationRole.MEMBER);
const { data: billingMode } = useOrgBillingMode();
Expand Down Expand Up @@ -169,7 +169,7 @@ export default function OrganizationSelector() {
const classes =
"flex h-full text-base py-0 text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700";
return (
<ContextMenu customClasses="w-64 left-0" menuEntries={entries}>
<ContextMenu customClasses="w-64 left-0 text-left" menuEntries={entries}>
<div className={`${classes} rounded-2xl pl-1`}>
<div className="py-1 pr-1 flex font-medium max-w-xs truncate">
<OrgIcon
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,19 @@ import { FC, Suspense, useEffect, useMemo, useState } from "react";
import { Redirect, useParams } from "react-router";
import { CircleSlash, Loader2Icon } from "lucide-react";
import dayjs from "dayjs";
import { usePrebuildLogsEmitter } from "../../../data/prebuilds/prebuild-logs-emitter";
import { usePrebuildLogsEmitter } from "../../data/prebuilds/prebuild-logs-emitter";
import React from "react";
import { useToast } from "../../../components/toasts/Toasts";
import { usePrebuildQuery, useTriggerPrebuildQuery, watchPrebuild } from "../../../data/prebuilds/prebuild-queries";
import { useToast } from "../../components/toasts/Toasts";
import { usePrebuildQuery, useTriggerPrebuildQuery, watchPrebuild } from "../../data/prebuilds/prebuild-queries";
import { LinkButton } from "@podkit/buttons/LinkButton";
import { repositoriesRoutes } from "../../repositories.routes";
import { repositoriesRoutes } from "../../repositories/repositories.routes";
import { LoadingState } from "@podkit/loading/LoadingState";
import Alert from "../../../components/Alert";
import { prebuildDisplayProps, prebuildStatusIconComponent } from "../../../projects/prebuild-utils";
import Alert from "../../components/Alert";
import { prebuildDisplayProps, prebuildStatusIconComponent } from "../../projects/prebuild-utils";
import { LoadingButton } from "@podkit/buttons/LoadingButton";
import { useConfiguration } from "../../../data/configurations/configuration-queries";
import { useConfiguration } from "../../data/configurations/configuration-queries";

const WorkspaceLogs = React.lazy(() => import("../../../components/WorkspaceLogs"));
const WorkspaceLogs = React.lazy(() => import("../../components/WorkspaceLogs"));

/**
* Formats a date. For today, it returns the time. For this year, it returns the month and day and time. Otherwise, it returns the full date and time.
Expand Down Expand Up @@ -90,7 +90,6 @@ export const PrebuildDetailPage: FC = () => {
}
}, [isTriggerError, triggerError, toast]);

// TODO: should reuse icon/description on prebuild list
const prebuildPhase = useMemo(() => {
const name = currentPrebuild?.status?.phase?.name;
if (!name) {
Expand Down Expand Up @@ -221,7 +220,7 @@ export const PrebuildDetailPage: FC = () => {
>{`Rerun Prebuild (${prebuild.ref})`}</LoadingButton>
<LinkButton
disabled={!prebuild?.id}
href={repositoriesRoutes.Detail(prebuild!.id!)}
href={repositoriesRoutes.Detail(configuration?.id!)}
variant="secondary"
>
View Imported Repository
Expand Down
1 change: 0 additions & 1 deletion components/dashboard/src/prebuilds/list/PrebuildList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ const PrebuildsListPage: FC = () => {
useDocumentTitle("Prebuilds");

const history = useHistory();

const params = useQueryParams();

const [statusFilter, setPrebuildsFilter] = useState(parseStatus(params));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ type Props = {
error: unknown;
};
export const PrebuildListErrorState: FC<Props> = ({ error }: Props) => {
const errorString = error instanceof Error ? error.message : String(error);

return (
<div className={cn("w-full flex justify-center mt-2 rounded-xl bg-pk-surface-secondary px-4 py-20")}>
<div className="flex flex-col justify-center items-center text-center space-y-4">
<Heading2>Prebuilds failed to load</Heading2>
<Subheading className="max-w-md">Error: {error}.</Subheading>
<Subheading className="max-w-md">Error: {errorString}.</Subheading>
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/**
* Copyright (c) 2024 Gitpod GmbH. All rights reserved.
* Licensed under the GNU Affero General Public License (AGPL).
* See License.AGPL.txt in the project root for license information.
*/

import { Button } from "@podkit/buttons/Button";
import { Popover, PopoverArrow, PopoverContent, PopoverTrigger } from "@podkit/popover/Popover";
import { Text } from "@podkit/typography/Text";
import { Truck } from "lucide-react";
import { PropsWithChildren, useCallback, useMemo, useState } from "react";
import { Link, useHistory } from "react-router-dom";
import { useFeatureFlag, useHasConfigurationsAndPrebuildsEnabled } from "../../data/featureflag-query";
import { useUserLoader } from "../../hooks/use-user-loader";
import { useUpdateCurrentUserMutation } from "../../data/current-user/update-mutation";
import dayjs from "dayjs";
import { trackEvent } from "../../Analytics";

const COACHMARK_KEY = "projects_configuration_migration";

type Props = PropsWithChildren<{}>;
export const ConfigurationsMigrationCoachmark = ({ children }: Props) => {
const [isOpen, setIsOpen] = useState(true);

const configurationsAndPrebuildsEnabled = useHasConfigurationsAndPrebuildsEnabled();
const prebuildsInMenu = useFeatureFlag("showPrebuildsMenuItem");

const history = useHistory();

const { user } = useUserLoader();
const { mutate: updateUser } = useUpdateCurrentUserMutation();

const dismiss = useCallback(() => {
updateUser(
{
additionalData: { profile: { coachmarksDismissals: { [COACHMARK_KEY]: dayjs().toISOString() } } },
},
{
onSettled: (_, error) => {
trackEvent("coachmark_dismissed", {
name: COACHMARK_KEY,
success: !(error instanceof Error),
});
},
},
);
}, [updateUser]);

const show = useMemo<boolean>(() => {
if (!isOpen || !user) {
return false;
}

// For the users signing up after our launch of configurations, don't show it
if (user.createdAt && user.createdAt.toDate() > new Date("2/21/2024")) {
return false;
}

// User already knows about the feature
if (history.location.pathname.startsWith("/repositories")) {
Copy link
Member Author

Choose a reason for hiding this comment

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

Could also dismiss automatically if a user visits /prebuilds. No strong opinion here.

dismiss();
return false;
}

return (
configurationsAndPrebuildsEnabled && prebuildsInMenu && !user.profile?.coachmarksDismissals[COACHMARK_KEY]
);
}, [configurationsAndPrebuildsEnabled, dismiss, history.location.pathname, isOpen, prebuildsInMenu, user]);

const handleClose = useCallback(() => {
setIsOpen(false);
// do not store the dismissal if the popover is not shown
if (show) {
dismiss();
}
}, [dismiss, show]);

return (
<Popover open={show}>
<PopoverTrigger onClick={handleClose}>{children}</PopoverTrigger>
<PopoverContent align={"start"} className="border-pk-border-base relative flex flex-col">
<PopoverArrow asChild>
<div className="mb-[6px] ml-2 inline-block overflow-hidden rotate-180 relative">
<div className="h-3 w-5 origin-bottom-left rotate-45 transform border border-pk-border-base bg-pk-surface-primary before:absolute before:bottom-0 before:left-0 before:w-full before:h-[1px] before:bg-pk-surface-primary" />
</div>
</PopoverArrow>

<Text className="flex flex-row gap-2 text-lg font-bold items-center pt-3">
<Truck /> Projects have moved
</Text>
<Text className="text-pk-content-secondary text-base pb-4 pt-2">
Projects are now called “
<Link to={"/repositories"} className="gp-link">
Imported Repositories.
</Link>
” You can find them in your organization menu.
</Text>
<Button className="self-end" variant={"secondary"} onClick={handleClose}>
Dismiss
</Button>
</PopoverContent>
</Popover>
);
};
3 changes: 3 additions & 0 deletions components/gitpod-protocol/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ export namespace AdditionalUserData {
return user;
}
}

// The format in which we store User Profiles in
export interface ProfileDetails {
// when was the last time the user updated their profile information or has been nudged to do so.
Expand All @@ -186,6 +187,8 @@ export interface ProfileDetails {
onboardedTimestamp?: string;
// Onboarding question about a user's company size
companySize?: string;
// key-value pairs of dialogs in the UI which should only appear once. The value usually is a timestamp of the last dismissal
coachmarksDismissals?: { [key: string]: string };
}

export interface EmailNotificationSettings {
Expand Down
6 changes: 6 additions & 0 deletions components/public-api/gitpod/v1/user.proto
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ message UpdateUserRequest {
optional string signup_goals_other = 10;
optional string onboarded_timestamp = 11;
optional string company_size = 12;
map<string, string> coachmarks_dismissals = 13;
Copy link
Member Author

Choose a reason for hiding this comment

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

I hope to use this for future dialogs of this nature to not bloat the profile object.

}
optional string email_address = 5;
optional EditorReference editor_settings = 6;
Expand Down Expand Up @@ -265,6 +266,11 @@ message User {
//
// +optional
string company_size = 12;

// key-value pairs of dialogs in the UI which should only appear once. The value usually is a timestamp of the last dismissal
//
// +optional
map<string, string> coachmarks_dismissals = 13;
}

// remembered workspace auto start options
Expand Down
Loading