Skip to content

Commit

Permalink
[dashboard] Add ItemsList component
Browse files Browse the repository at this point in the history
  • Loading branch information
corneliusludmann committed Jun 10, 2021
1 parent 94d61c7 commit 240dcbf
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 138 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file.

## June 2021

- Adding `ItemsList` component as a more maintainable and consistent way to render a list of workspaces, git integrations, environment variables, etc. ([#4454](https://github.com/gitpod-io/gitpod/pull/4454))
- Improve backup stability when pods get evicted ([#4405](https://github.com/gitpod-io/gitpod/pull/4405))
- Fix text color in workspaces list for dark theme ([#4410](https://github.com/gitpod-io/gitpod/pull/4410))
- Better reflect incremental prebuilds in prebuilt workspace logs ([#4293](https://github.com/gitpod-io/gitpod/pull/4293))
Expand Down
55 changes: 55 additions & 0 deletions components/dashboard/src/components/ItemsList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* Copyright (c) 2021 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 ContextMenu, { ContextMenuEntry } from "./ContextMenu"

export function ItemsList(props: {
children?: React.ReactNode;
className?: string;
}) {
return <div className={`flex flex-col space-y-2 ${props.className || ""}`}>
{props.children}
</div>;
}

export function Item(props: {
children?: React.ReactNode;
className?: string;
header?: boolean;
}) {
const headerClassName = "text-sm text-gray-400 border-t border-b border-gray-200 dark:border-gray-800";
const notHeaderClassName = "rounded-xl hover:bg-gray-100 dark:hover:bg-gray-800 focus:bg-gitpod-kumquat-light";
return <div className={`flex flex-grow flex-row w-full px-3 py-3 justify-between transition ease-in-out group ${props.header ? headerClassName : notHeaderClassName} ${props.className || ""}`}>
{props.children}
</div>;
}

export function ItemField(props: {
children?: React.ReactNode;
className?: string;
}) {
return <div className={`flex-grow my-auto mx-1 ${props.className || ""}`}>
{props.children}
</div>;
}

export function ItemFieldIcon(props: {
children?: React.ReactNode;
className?: string;
}) {
return <div className={`flex self-center w-8 ${props.className || ""}`}>
{props.children}
</div>;
}

export function ItemFieldContextMenu(props: {
menuEntries?: ContextMenuEntry[];
className?: string;
}) {
return <div className={`flex self-center hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md cursor-pointer w-8 ${props.className || ""}`}>
{!props.menuEntries ? null : <ContextMenu menuEntries={props.menuEntries} />}
</div>;
}
68 changes: 31 additions & 37 deletions components/dashboard/src/settings/EnvironmentVariables.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
*/

import { UserEnvVarValue } from "@gitpod/gitpod-protocol";
import { useEffect, useRef, useState } from "react";
import ContextMenu from "../components/ContextMenu";
import React, { useEffect, useRef, useState } from "react";
import ConfirmationModal from "../components/ConfirmationModal";
import { Item, ItemField, ItemFieldContextMenu, ItemsList } from "../components/ItemsList";
import Modal from "../components/Modal";
import { getGitpodService } from "../service/service";
import { PageWithSubMenu } from "../components/PageWithSubMenu";
import { getGitpodService } from "../service/service";
import settingsMenu from "./settings-menu";
import ConfirmationModal from "../components/ConfirmationModal";

interface EnvVarModalProps {
envVar: UserEnvVarValue;
Expand Down Expand Up @@ -180,6 +180,8 @@ export default function EnvVars() {
return '';
};



return <PageWithSubMenu subMenu={settingsMenu} title='Variables' subtitle='Configure environment variables for all workspaces.'>
{isAddEnvVarModalVisible && <AddEnvVarModal
save={save}
Expand Down Expand Up @@ -209,39 +211,31 @@ export default function EnvVars() {
<button onClick={add}>New Variable</button>
</div>
</div>
: <div className="space-y-2">
<div className="flex flex-col space-y-2">
<div className="px-3 py-3 flex justify-between space-x-2 text-sm text-gray-400 border-t border-b border-gray-200 dark:border-gray-800">
<div className="w-5/12">Name</div>
<div className="w-5/12">Scope</div>
<div className="w-2/12"></div>
</div>
</div>
<div className="flex flex-col">
{envVars.map(variable => {
return <div className="rounded-xl whitespace-nowrap flex space-x-2 py-3 px-3 w-full justify-between hover:bg-gray-100 dark:hover:bg-gray-800 focus:bg-gitpod-kumquat-light transition ease-in-out group">
<div className="w-5/12 m-auto overflow-ellipsis truncate">{variable.name}</div>
<div className="w-5/12 m-auto overflow-ellipsis truncate text-sm text-gray-400">{variable.repositoryPattern}</div>
<div className="w-2/12 flex justify-end">
<div className="flex w-8 self-center hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md cursor-pointer opacity-0 group-hover:opacity-100">
<ContextMenu menuEntries={[
{
title: 'Edit',
onClick: () => edit(variable),
separator: true
},
{
title: 'Delete',
customFontStyle: 'text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300',
onClick: () => confirmDeleteVariable(variable)
},
]} />
</div>
</div>
</div>
})}
</div>
</div>
: <ItemsList>
<Item header={true}>
<ItemField className="w-5/12">Name</ItemField>
<ItemField className="w-5/12">Scope</ItemField>
<ItemFieldContextMenu />
</Item>
{envVars.map(variable => {
return <Item className="whitespace-nowrap">
<ItemField className="w-5/12 overflow-ellipsis truncate">{variable.name}</ItemField>
<ItemField className="w-5/12 overflow-ellipsis truncate text-sm text-gray-400">{variable.repositoryPattern}</ItemField>
<ItemFieldContextMenu menuEntries={[
{
title: 'Edit',
onClick: () => edit(variable),
separator: true
},
{
title: 'Delete',
customFontStyle: 'text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300',
onClick: () => confirmDeleteVariable(variable)
},
]} />
</Item>
})}
</ItemsList>
}
</PageWithSubMenu>;
}
82 changes: 37 additions & 45 deletions components/dashboard/src/settings/Integrations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,20 @@
import { AuthProviderEntry, AuthProviderInfo } from "@gitpod/gitpod-protocol";
import { SelectAccountPayload } from "@gitpod/gitpod-protocol/lib/auth";
import React, { useContext, useEffect, useState } from "react";
import ContextMenu, { ContextMenuEntry } from "../components/ContextMenu";
import { getGitpodService, gitpodHostUrl } from "../service/service";
import { UserContext } from "../user-context";
import copy from '../images/copy.svg';
import exclamation from '../images/exclamation.svg';
import AlertBox from "../components/AlertBox";
import Modal from "../components/Modal";
import { openAuthorizeWindow } from "../provider-utils";
import CheckBox from '../components/CheckBox';
import ConfirmationModal from "../components/ConfirmationModal";
import { ContextMenuEntry } from "../components/ContextMenu";
import { Item, ItemField, ItemFieldContextMenu, ItemFieldIcon, ItemsList } from "../components/ItemsList";
import Modal from "../components/Modal";
import { PageWithSubMenu } from "../components/PageWithSubMenu";
import settingsMenu from "./settings-menu";
import copy from '../images/copy.svg';
import exclamation from '../images/exclamation.svg';
import { openAuthorizeWindow } from "../provider-utils";
import { getGitpodService, gitpodHostUrl } from "../service/service";
import { UserContext } from "../user-context";
import { SelectAccountModal } from "./SelectAccountModal";
import ConfirmationModal from "../components/ConfirmationModal";
import settingsMenu from "./settings-menu";

export default function Integrations() {

Expand Down Expand Up @@ -280,34 +281,30 @@ function GitProviders() {

<h3>Git Providers</h3>
<h2>Manage permissions for git providers.</h2>
<div className="flex flex-col pt-6 space-y-2">
<ItemsList className="pt-6">
{authProviders && authProviders.map(ap => (
<div key={"ap-" + ap.authProviderId} className="flex-grow flex flex-row hover:bg-gray-100 dark:hover:bg-gray-800 rounded-xl h-16 w-full transition ease-in-out group">
<div className="px-4 self-center w-1/12">
<div className={"rounded-full w-3 h-3 text-sm align-middle " + (isConnected(ap.authProviderId) ? "bg-green-500" : "bg-gray-400")}>
<Item key={"ap-" + ap.authProviderId} className="h-16">
<ItemFieldIcon>
<div className={"rounded-full w-3 h-3 text-sm align-middle m-auto " + (isConnected(ap.authProviderId) ? "bg-green-500" : "bg-gray-400")}>
&nbsp;
</div>
</div>
<div className="p-0 my-auto flex flex-col xl:w-3/12 w-4/12">
</ItemFieldIcon>
<ItemField className="w-4/12 xl:w-3/12 flex flex-col">
<span className="my-auto font-medium truncate overflow-ellipsis">{ap.authProviderType}</span>
<span className="text-sm my-auto text-gray-400 truncate overflow-ellipsis">{ap.host}</span>
</div>
<div className="p-0 my-auto flex flex-col xl:w-2/12 w-6/12">
</ItemField>
<ItemField className="w-6/12 xl:w-3/12 flex flex-col">
<span className="my-auto truncate text-gray-500 overflow-ellipsis">{getUsername(ap.authProviderId) || "–"}</span>
<span className="text-sm my-auto text-gray-400">Username</span>
</div>
<div className="p-0 my-auto hidden xl:flex xl:flex-col xl:w-5/12">
</ItemField>
<ItemField className="hidden xl:w-5/12 xl:flex xl:flex-col">
<span className="my-auto truncate text-gray-500 overflow-ellipsis">{getPermissions(ap.authProviderId)?.join(", ") || "–"}</span>
<span className="text-sm my-auto text-gray-400">Permissions</span>
</div>
<div className="my-auto flex w-1/12 mr-4 opacity-0 group-hover:opacity-100 justify-end">
<div className="self-center hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md cursor-pointer w-8">
<ContextMenu menuEntries={gitProviderMenu(ap)} />
</div>
</div>
</div>
</ItemField>
<ItemFieldContextMenu menuEntries={gitProviderMenu(ap)} />
</Item>
))}
</div>
</ItemsList>
</div>);
}

Expand Down Expand Up @@ -397,29 +394,24 @@ function GitIntegrations() {
</div>
</div>
)}
<div className="items pt-6 space-y-2">
<ItemsList className="pt-6">
{providers && providers.map(ap => (
<div key={"ap-" + ap.id} className="item h-16">

<div className="px-4 self-center w-1/12">
<div className={"rounded-full w-3 h-3 text-sm align-middle " + (ap.status === "verified" ? "bg-green-500" : "bg-gray-400")}>
<Item key={"ap-" + ap.id} className="h-16">
<ItemFieldIcon>
<div className={"rounded-full w-3 h-3 text-sm align-middle m-auto " + (ap.status === "verified" ? "bg-green-500" : "bg-gray-400")}>
&nbsp;
</div>
</div>
<div className="p-0 my-auto flex flex-col w-3/12">
<span className="my-auto font-medium truncate overflow-ellipsis">{ap.type}</span>
</div>
<div className="p-0 my-auto flex flex-col w-7/12">
</ItemFieldIcon>
<ItemField className="w-3/12 flex flex-col">
<span className="font-medium truncate overflow-ellipsis">{ap.type}</span>
</ItemField>
<ItemField className="w-7/12 flex flex-col">
<span className="my-auto truncate text-gray-500 overflow-ellipsis">{ap.host}</span>
</div>
<div className="my-auto flex w-1/12 mr-4 opacity-0 group-hover:opacity-100 justify-end">
<div className="item-context-menu">
<ContextMenu menuEntries={gitProviderMenu(ap)} />
</div>
</div>
</div>
</ItemField>
<ItemFieldContextMenu menuEntries={gitProviderMenu(ap)} />
</Item>
))}
</div>
</ItemsList>
</div>);
}

Expand Down
75 changes: 36 additions & 39 deletions components/dashboard/src/workspaces/WorkspaceEntry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@

import { CommitContext, Workspace, WorkspaceInfo, WorkspaceInstance, WorkspaceInstancePhase } from '@gitpod/gitpod-protocol';
import { GitpodHostUrl } from '@gitpod/gitpod-protocol/lib/util/gitpod-host-url';
import ContextMenu, { ContextMenuEntry } from '../components/ContextMenu';
import moment from 'moment';
import { useState } from 'react';
import { WorkspaceModel } from './workspace-model';
import React, { useState } from 'react';
import ConfirmationModal from '../components/ConfirmationModal';
import { ContextMenuEntry } from '../components/ContextMenu';
import { Item, ItemField, ItemFieldContextMenu, ItemFieldIcon } from '../components/ItemsList';
import PendingChangesDropdown from '../components/PendingChangesDropdown';
import Tooltip from '../components/Tooltip';
import ConfirmationModal from '../components/ConfirmationModal';
import { WorkspaceModel } from './workspace-model';

function getLabel(state: WorkspaceInstancePhase) {
return state.substr(0,1).toLocaleUpperCase() + state.substr(1);
Expand Down Expand Up @@ -80,36 +81,31 @@ export function WorkspaceEntry({ desc, model, isAdmin, stopWorkspace }: Props) {
);
}
const project = getProject(ws);
return <div>
<div className="rounded-xl whitespace-nowrap flex space-x-2 py-6 px-6 w-full justify-between hover:bg-gray-100 dark:hover:bg-gray-800 focus:bg-gitpod-kumquat-light group">
<div className="pr-3 self-center">
<WorkspaceStatusIndicator instance={desc?.latestInstance} />
</div>
<div className="flex flex-col w-3/12">
<a href={startUrl.toString()}><div className="font-medium text-gray-800 dark:text-gray-200 truncate hover:text-blue-600 dark:hover:text-blue-400">{ws.id}</div></a>
<a href={project ? 'https://' + project : undefined}><div className="text-sm overflow-ellipsis truncate text-gray-400 dark:text-gray-500 hover:text-blue-600 dark:hover:text-blue-400">{project || 'Unknown'}</div></a>
</div>
<div className="flex w-4/12 truncate overflow-ellipsis">
<div className="flex flex-col">
<div className="text-gray-500 dark:text-gray-400 overflow-ellipsis truncate">{ws.description}</div>
<a href={ws.contextURL}>
<div className="text-sm text-gray-400 dark:text-gray-500 overflow-ellipsis truncate hover:text-blue-600 dark:hover:text-blue-400">{ws.contextURL}</div>
</a>
</div>
</div>
<div className="flex flex-col items-start w-2/12">
<div className="text-gray-500 dark:text-gray-400 truncate">{currentBranch}</div>
<PendingChangesDropdown workspaceInstance={desc.latestInstance} />
</div>
<div className="flex w-2/12 self-center">
<Tooltip content={`Created ${moment(desc.workspace.creationTime).fromNow()}`}>
<div className="text-sm w-full text-gray-400 truncate">{moment(WorkspaceInfo.lastActiveISODate(desc)).fromNow()}</div>
</Tooltip>
</div>
<div className="flex w-8 self-center hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md cursor-pointer opacity-0 group-hover:opacity-100">
<ContextMenu menuEntries={menuEntries} />
</div>
</div>

return <Item className="whitespace-nowrap py-6 px-6">
<ItemFieldIcon>
<WorkspaceStatusIndicator instance={desc?.latestInstance} />
</ItemFieldIcon>
<ItemField className="w-3/12 flex flex-col">
<a href={startUrl.toString()}><div className="font-medium text-gray-800 dark:text-gray-200 truncate hover:text-blue-600 dark:hover:text-blue-400">{ws.id}</div></a>
<a href={project ? 'https://' + project : undefined}><div className="text-sm overflow-ellipsis truncate text-gray-400 dark:text-gray-500 hover:text-blue-600 dark:hover:text-blue-400">{project || 'Unknown'}</div></a>
</ItemField>
<ItemField className="w-4/12 flex flex-col">
<div className="text-gray-500 dark:text-gray-400 overflow-ellipsis truncate">{ws.description}</div>
<a href={ws.contextURL}>
<div className="text-sm text-gray-400 dark:text-gray-500 overflow-ellipsis truncate hover:text-blue-600 dark:hover:text-blue-400">{ws.contextURL}</div>
</a>
</ItemField>
<ItemField className="w-2/12 flex flex-col">
<div className="text-gray-500 dark:text-gray-400 overflow-ellipsis truncate">{currentBranch}</div>
<PendingChangesDropdown workspaceInstance={desc.latestInstance} />
</ItemField>
<ItemField className="w-2/12 flex">
<Tooltip content={`Created ${moment(desc.workspace.creationTime).fromNow()}`}>
<div className="text-sm w-full text-gray-400 overflow-ellipsis truncate">{moment(WorkspaceInfo.lastActiveISODate(desc)).fromNow()}</div>
</Tooltip>
</ItemField>
<ItemFieldContextMenu menuEntries={menuEntries} />
{isModalVisible && <ConfirmationModal
title="Delete Workspace"
areYouSureText="Are you sure you want to delete this workspace?"
Expand All @@ -122,7 +118,7 @@ export function WorkspaceEntry({ desc, model, isAdmin, stopWorkspace }: Props) {
onClose={() => setModalVisible(false)}
onConfirm={() => model.deleteWorkspace(ws.id)}
/>}
</div>;
</Item>;
}

export function getProject(ws: Workspace) {
Expand Down Expand Up @@ -154,8 +150,9 @@ export function WorkspaceStatusIndicator({instance}: {instance?: WorkspaceInstan
break;
}
}
return <Tooltip content={getLabel(state)}>
<div className={stateClassName}>
</div>
</Tooltip>;
return <div className="m-auto">
<Tooltip content={getLabel(state)}>
<div className={stateClassName} />
</Tooltip>
</div>;
}
Loading

0 comments on commit 240dcbf

Please sign in to comment.