Skip to content

Adding a Button component #16592

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

Merged
merged 8 commits into from
Mar 2, 2023
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
98 changes: 98 additions & 0 deletions components/dashboard/src/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/**
* Copyright (c) 2023 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 classNames from "classnames";
import { FC, RefObject } from "react";
import SpinnerWhite from "../icons/SpinnerWhite.svg";

type Props = {
// TODO: determine if we want danger.secondary
type?: "primary" | "secondary" | "danger" | "danger.secondary";
// TODO: determine how to handle small/medium (block does w-full atm)
size?: "small" | "medium" | "block";
disabled?: boolean;
loading?: boolean;
className?: string;
autoFocus?: boolean;
ref?: RefObject<HTMLButtonElement>;
htmlType?: "button" | "submit" | "reset";
onClick?: ButtonOnClickHandler;
};

// Allow w/ or w/o handling event argument
type ButtonOnClickHandler = React.DOMAttributes<HTMLButtonElement>["onClick"] | (() => void);

export const Button: FC<Props> = ({
type = "primary",
className,
htmlType,
disabled = false,
loading = false,
autoFocus = false,
ref,
size,
children,
onClick,
}) => {
return (
<button
type={htmlType}
className={classNames(
"cursor-pointer px-4 py-2 my-auto",
"text-sm font-medium",
"rounded-md focus:outline-none focus:ring transition ease-in-out",
type === "primary"
? [
"bg-green-600 dark:bg-green-700 hover:bg-green-700 dark:hover:bg-green-600",
"text-gray-100 dark:text-green-100",
]
: null,
type === "secondary"
? [
"bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600",
"text-gray-500 dark:text-gray-100 hover:text-gray-600",
]
: null,
type === "danger" ? ["bg-red-600 hover:bg-red-700", "text-gray-100 dark:text-red-100"] : null,
type === "danger.secondary"
? [
"bg-red-50 dark:bg-red-300 hover:bg-red-100 dark:hover:bg-red-200",
"text-red-600 hover:text-red-700",
]
: null,
{
"w-full": size === "block",
"cursor-default opacity-50 pointer-events-none": disabled || loading,
},
className,
)}
ref={ref}
disabled={disabled}
autoFocus={autoFocus}
onClick={onClick}
>
<ButtonContent loading={loading}>{children}</ButtonContent>
</button>
);
};

// TODO: Consider making this a LoadingButton variant instead
type ButtonContentProps = {
loading: boolean;
};
const ButtonContent: FC<ButtonContentProps> = ({ loading, children }) => {
if (!loading) {
return <>{children}</>;
}

return (
<div className="flex items-center justify-center space-x-2">
{/* TODO: This spinner doesn't look right - use a solid white instead? */}
<img className="h-4 w-4 animate-spin" src={SpinnerWhite} alt="loading spinner" />
<span>{children}</span>
</div>
);
};
9 changes: 5 additions & 4 deletions components/dashboard/src/components/ConfirmationModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import Alert from "./Alert";
import Modal from "./Modal";
import { useRef, useEffect } from "react";
import { Button } from "./Button";

export default function ConfirmationModal(props: {
title?: string;
Expand All @@ -23,12 +24,12 @@ export default function ConfirmationModal(props: {
const cancelButtonRef = useRef<HTMLButtonElement>(null);

const buttons = [
<button className="secondary" onClick={props.onClose} autoFocus ref={cancelButtonRef}>
<Button type="secondary" onClick={props.onClose} autoFocus ref={cancelButtonRef}>
Cancel
</button>,
<button className="ml-2 danger" onClick={props.onConfirm} disabled={props.buttonDisabled}>
</Button>,
<Button type="danger" className="ml-2" onClick={props.onConfirm} disabled={props.buttonDisabled}>
{props.buttonText || "Yes, I'm Sure"}
</button>,
</Button>,
];

const buttonDisabled = useRef(props.buttonDisabled);
Expand Down
3 changes: 3 additions & 0 deletions components/dashboard/src/icons/SpinnerWhite.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 4 additions & 3 deletions components/dashboard/src/onboarding/OnboardingStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import { FC, FormEvent, useCallback } from "react";
import Alert from "../components/Alert";
import { Button } from "../components/Button";

type Props = {
title: string;
Expand Down Expand Up @@ -48,10 +49,10 @@ export const OnboardingStep: FC<Props> = ({

{error && <Alert type="error">{error}</Alert>}

<div>
<button type="submit" disabled={!isValid || isSaving} className="w-full mt-8">
<div className="mt-8">
<Button htmlType="submit" disabled={!isValid || isSaving} size="block">
Continue
</button>
</Button>
</div>
</form>
</div>
Expand Down
9 changes: 4 additions & 5 deletions components/dashboard/src/user-settings/Account.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { UserContext } from "../user-context";
import ConfirmationModal from "../components/ConfirmationModal";
import ProfileInformation, { ProfileState } from "./ProfileInformation";
import { PageWithSettingsSubMenu } from "./PageWithSettingsSubMenu";
import { Button } from "../components/Button";

export default function Account() {
const { user, setUser } = useContext(UserContext);
Expand Down Expand Up @@ -84,19 +85,17 @@ export default function Account() {
updated={updated}
>
<div className="flex flex-row mt-8">
<button className="primary" onClick={saveProfileState}>
Update Profile
</button>
<Button onClick={saveProfileState}>Update Profile</Button>
</div>
</ProfileInformation>
</form>
<h3 className="mt-12">Delete Account</h3>
<p className="text-base text-gray-500 pb-4">
This action will remove all the data associated with your account in Gitpod.
</p>
<button className="danger secondary" onClick={() => setModal(true)}>
<Button type="danger.secondary" onClick={() => setModal(true)}>
Delete Account
</button>
</Button>
</PageWithSettingsSubMenu>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import Modal, { ModalBody, ModalFooter, ModalHeader } from "../components/Modal"
import { getGitpodService } from "../service/service";
import { PageWithSettingsSubMenu } from "./PageWithSettingsSubMenu";
import { EnvironmentVariableEntry } from "./EnvironmentVariableEntry";
import { Button } from "../components/Button";

interface EnvVarModalProps {
envVar: UserEnvVarValue;
Expand Down Expand Up @@ -101,12 +102,10 @@ function AddEnvVarModal(p: EnvVarModalProps) {
</div>
</ModalBody>
<ModalFooter>
<button className="secondary" onClick={p.onClose}>
<Button type="secondary" onClick={p.onClose}>
Cancel
</button>
<button className="ml-2" onClick={save}>
{isNew ? "Add" : "Update"} Variable
</button>
</Button>
<Button onClick={save}>{isNew ? "Add" : "Update"} Variable</Button>
</ModalFooter>
</Modal>
);
Expand Down
25 changes: 7 additions & 18 deletions components/dashboard/src/workspaces/CreateWorkspacePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ import { StartWorkspaceOptions } from "../start/start-workspace-options";
import { StartWorkspaceError } from "../start/StartPage";
import { useCurrentUser } from "../user-context";
import { SelectAccountModal } from "../user-settings/SelectAccountModal";
import Spinner from "../icons/Spinner.svg";
import { useFeatureFlags } from "../contexts/FeatureFlagContext";
import { useCurrentTeam } from "../teams/teams-context";
import { useCreateWorkspaceMutation } from "../data/workspaces/create-workspace-mutation";
import { Button } from "../components/Button";

export const useNewCreateWorkspacePage = () => {
const { startWithOptions } = useFeatureFlags();
Expand Down Expand Up @@ -104,6 +104,7 @@ export function CreateWorkspacePage() {
[createWorkspaceMutation, history, repo, selectedIde, selectedWsClass, team?.id, useLatestIde],
);

// Need a wrapper here so we call createWorkspace w/o any arguments
const onClickCreate = useCallback(() => createWorkspace(), [createWorkspace]);

if (SelectAccountPayload.is(selectAccountError)) {
Expand Down Expand Up @@ -147,25 +148,13 @@ export function CreateWorkspacePage() {
</div>
</div>
<div className="w-full flex justify-end mt-6 space-x-2 px-6">
<button
<Button
onClick={onClickCreate}
disabled={
createWorkspaceMutation.isLoading ||
!repo ||
repo.length === 0 ||
!!errorIde ||
!!errorWsClass
}
loading={createWorkspaceMutation.isLoading}
disabled={!repo || repo.length === 0 || !!errorIde || !!errorWsClass}
>
{createWorkspaceMutation.isLoading ? (
<div className="flex">
<img className="h-4 w-4 animate-spin" src={Spinner} alt="loading spinner" />
<span className="pl-2">Creating Workspace ...</span>
</div>
) : (
"New Workspace"
)}
</button>
{createWorkspaceMutation.isLoading ? "Creating Workspace ..." : "New Workspace"}
</Button>
</div>
<div>
<StatusMessage
Expand Down
5 changes: 3 additions & 2 deletions components/dashboard/src/workspaces/WorkspacesSearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { FunctionComponent, useContext } from "react";
import DropDown from "../components/DropDown";
import { StartWorkspaceModalContext, StartWorkspaceModalKeyBinding } from "./start-workspace-modal-context";
import search from "../icons/search.svg";
import { Button } from "../components/Button";

type WorkspacesSearchBarProps = {
searchTerm: string;
Expand Down Expand Up @@ -67,9 +68,9 @@ export const WorkspacesSearchBar: FunctionComponent<WorkspacesSearchBarProps> =
]}
/>
</div>
<button onClick={() => setStartWorkspaceModalProps({})} className="ml-2">
<Button onClick={() => setStartWorkspaceModalProps({})} className="ml-2">
New Workspace <span className="opacity-60 hidden md:inline">{StartWorkspaceModalKeyBinding}</span>
</button>
</Button>
</div>
);
};