Skip to content

Commit

Permalink
[public-api] Add installation service (#19150)
Browse files Browse the repository at this point in the history
* Add installation service

* Fix dashboard

* Fix

* Fix

---------

Co-authored-by: Huiwen <[email protected]>
  • Loading branch information
jeanp413 and mustard-mh authored Nov 29, 2023
1 parent d327e5c commit a997229
Show file tree
Hide file tree
Showing 27 changed files with 2,845 additions and 779 deletions.
25 changes: 14 additions & 11 deletions components/dashboard/src/admin/BlockedEmailDomains.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ import { ItemFieldContextMenu } from "../components/ItemsList";
import Modal, { ModalBody, ModalFooter, ModalHeader } from "../components/Modal";
import { CheckboxInputField } from "../components/forms/CheckboxInputField";
import searchIcon from "../icons/search.svg";
import { getGitpodService } from "../service/service";
import { AdminPageHeader } from "./AdminPageHeader";
import Pagination from "../Pagination/Pagination";
import { Button } from "@podkit/buttons/Button";
import { installationClient } from "../service/public-api";
import { ListBlockedEmailDomainsResponse } from "@gitpod/public-api/lib/gitpod/v1/installation_pb";

export function BlockedEmailDomains() {
return (
Expand All @@ -27,7 +28,7 @@ export function BlockedEmailDomains() {
}

function useBlockedEmailDomains() {
return useQuery(["blockedEmailDomains"], () => getGitpodService().server.adminGetBlockedEmailDomains(), {
return useQuery(["blockedEmailDomains"], () => installationClient.listBlockedEmailDomains({}), {
staleTime: 1000 * 60 * 5, // 5min
});
}
Expand All @@ -37,19 +38,21 @@ function useUpdateBlockedEmailDomainMutation() {
const blockedEmailDomains = useBlockedEmailDomains();
return useMutation(
async (blockedDomain: EmailDomainFilterEntry) => {
await getGitpodService().server.adminSaveBlockedEmailDomain(blockedDomain);
await installationClient.createBlockedEmailDomain({
domain: blockedDomain.domain,
negative: blockedDomain.negative ?? false,
});
},
{
onSuccess: (_, blockedDomain) => {
const updated = [];
for (const entry of blockedEmailDomains.data || []) {
const data = new ListBlockedEmailDomainsResponse(blockedEmailDomains.data);
data.blockedEmailDomains.map((entry) => {
if (entry.domain !== blockedDomain.domain) {
updated.push(entry);
} else {
updated.push(blockedDomain);
return entry;
}
}
queryClient.setQueryData(["blockedEmailDomains"], updated);
return blockedDomain;
});
queryClient.setQueryData(["blockedEmailDomains"], data);
blockedEmailDomains.refetch();
},
},
Expand All @@ -74,7 +77,7 @@ export function BlockedEmailDomainsList(props: Props) {
if (!blockedEmailDomains.data) {
return [];
}
return blockedEmailDomains.data.filter((entry) =>
return blockedEmailDomains.data.blockedEmailDomains.filter((entry) =>
entry.domain.toLowerCase().includes(searchTerm.toLowerCase()),
);
}, [blockedEmailDomains.data, searchTerm]);
Expand Down
69 changes: 43 additions & 26 deletions components/dashboard/src/admin/BlockedRepositories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,8 @@
* See License.AGPL.txt in the project root for license information.
*/

import { AdminGetListResult } from "@gitpod/gitpod-protocol";
import { useCallback, useEffect, useRef, useState } from "react";
import { getGitpodService } from "../service/service";
import { AdminPageHeader } from "./AdminPageHeader";
import { BlockedRepository } from "@gitpod/gitpod-protocol/lib/blocked-repositories-protocol";
import ConfirmationModal from "../components/ConfirmationModal";
import Modal, { ModalBody, ModalFooter, ModalHeader } from "../components/Modal";
import { CheckboxInputField } from "../components/forms/CheckboxInputField";
Expand All @@ -18,6 +15,9 @@ import Alert from "../components/Alert";
import { SpinnerLoader } from "../components/Loader";
import searchIcon from "../icons/search.svg";
import { Button } from "@podkit/buttons/Button";
import { installationClient } from "../service/public-api";
import { Sort, SortOrder } from "@gitpod/public-api/lib/gitpod/v1/sorting_pb";
import { BlockedRepository, ListBlockedRepositoriesResponse } from "@gitpod/public-api/lib/gitpod/v1/installation_pb";

export function BlockedRepositories() {
return (
Expand All @@ -33,27 +33,40 @@ type ExistingBlockedRepository = Pick<BlockedRepository, "id" | "urlRegexp" | "b
interface Props {}

export function BlockedRepositoriesList(props: Props) {
const [searchResult, setSearchResult] = useState<AdminGetListResult<BlockedRepository>>({ rows: [], total: 0 });
const [searchResult, setSearchResult] = useState<ListBlockedRepositoriesResponse>(
new ListBlockedRepositoriesResponse({
blockedRepositories: [],
}),
);
const [queryTerm, setQueryTerm] = useState("");
const [searching, setSearching] = useState(false);

const [isAddModalVisible, setAddModalVisible] = useState(false);
const [isDeleteModalVisible, setDeleteModalVisible] = useState(false);

const [currentBlockedRepository, setCurrentBlockedRepository] = useState<ExistingBlockedRepository>({
id: 0,
urlRegexp: "",
blockUser: false,
});
const [currentBlockedRepository, setCurrentBlockedRepository] = useState<BlockedRepository>(
new BlockedRepository({
id: 0,
urlRegexp: "",
blockUser: false,
}),
);

const search = async () => {
setSearching(true);
try {
const result = await getGitpodService().server.adminGetBlockedRepositories({
limit: 100,
orderBy: "urlRegexp",
offset: 0,
orderDir: "asc",
const result = await installationClient.listBlockedRepositories({
// Don't need, added it in json-rpc implement to make life easier.
// pagination: new PaginationRequest({
// token: Buffer.from(JSON.stringify({ offset: 0 })).toString("base64"),
// pageSize: 100,
// }),
sort: [
new Sort({
field: "urlRegexp",
order: SortOrder.ASC,
}),
],
searchTerm: queryTerm,
});
setSearchResult(result);
Expand All @@ -67,19 +80,21 @@ export function BlockedRepositoriesList(props: Props) {
}, []);

const add = () => {
setCurrentBlockedRepository({
id: 0,
urlRegexp: "",
blockUser: false,
});
setCurrentBlockedRepository(
new BlockedRepository({
id: 0,
urlRegexp: "",
blockUser: false,
}),
);
setAddModalVisible(true);
};

const save = async (blockedRepository: NewBlockedRepository) => {
await getGitpodService().server.adminCreateBlockedRepository(
blockedRepository.urlRegexp,
blockedRepository.blockUser,
);
await installationClient.createBlockedRepository({
urlRegexp: blockedRepository.urlRegexp ?? "",
blockUser: blockedRepository.blockUser ?? false,
});
setAddModalVisible(false);
search();
};
Expand All @@ -91,11 +106,13 @@ export function BlockedRepositoriesList(props: Props) {
};

const deleteBlockedRepository = async (blockedRepository: ExistingBlockedRepository) => {
await getGitpodService().server.adminDeleteBlockedRepository(blockedRepository.id);
await installationClient.deleteBlockedRepository({
blockedRepositoryId: blockedRepository.id,
});
search();
};

const confirmDeleteBlockedRepository = (blockedRepository: ExistingBlockedRepository) => {
const confirmDeleteBlockedRepository = (blockedRepository: BlockedRepository) => {
setCurrentBlockedRepository(blockedRepository);
setAddModalVisible(false);
setDeleteModalVisible(true);
Expand Down Expand Up @@ -158,7 +175,7 @@ export function BlockedRepositoriesList(props: Props) {
<div className="w-1/12">Block Users</div>
<div className="w-1/12"></div>
</div>
{searchResult.rows.map((br) => (
{searchResult.blockedRepositories.map((br) => (
<BlockedRepositoryEntry br={br} confirmedDelete={confirmDeleteBlockedRepository} />
))}
</div>
Expand Down
4 changes: 3 additions & 1 deletion components/dashboard/src/data/setup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,14 @@ import * as AuthProviderClasses from "@gitpod/public-api/lib/gitpod/v1/authprovi
import * as EnvVarClasses from "@gitpod/public-api/lib/gitpod/v1/envvar_pb";
import * as PrebuildClasses from "@gitpod/public-api/lib/gitpod/v1/prebuild_pb";
import * as VerificationClasses from "@gitpod/public-api/lib/gitpod/v1/verification_pb";
import * as InstallationClasses from "@gitpod/public-api/lib/gitpod/v1/installation_pb";
import * as SCMClasses from "@gitpod/public-api/lib/gitpod/v1/scm_pb";
import * as SSHClasses from "@gitpod/public-api/lib/gitpod/v1/ssh_pb";

// This is used to version the cache
// If data we cache changes in a non-backwards compatible way, increment this version
// That will bust any previous cache versions a client may have stored
const CACHE_VERSION = "11";
const CACHE_VERSION = "12";

export function noPersistence(queryKey: QueryKey): QueryKey {
return [...queryKey, "no-persistence"];
Expand Down Expand Up @@ -154,6 +155,7 @@ function initializeMessages() {
...Object.values(EnvVarClasses),
...Object.values(PrebuildClasses),
...Object.values(VerificationClasses),
...Object.values(InstallationClasses),
...Object.values(SCMClasses),
...Object.values(SSHClasses),
];
Expand Down
101 changes: 101 additions & 0 deletions components/dashboard/src/service/json-rpc-installation-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
* 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 { CallOptions, PromiseClient } from "@connectrpc/connect";
import { PartialMessage } from "@bufbuild/protobuf";
import { InstallationService } from "@gitpod/public-api/lib/gitpod/v1/installation_connect";
import {
ListBlockedRepositoriesRequest,
ListBlockedRepositoriesResponse,
CreateBlockedRepositoryRequest,
CreateBlockedRepositoryResponse,
DeleteBlockedRepositoryRequest,
DeleteBlockedRepositoryResponse,
ListBlockedEmailDomainsRequest,
ListBlockedEmailDomainsResponse,
CreateBlockedEmailDomainRequest,
CreateBlockedEmailDomainResponse,
} from "@gitpod/public-api/lib/gitpod/v1/installation_pb";
import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
import { getGitpodService } from "./service";
import { converter } from "./public-api";
import { PaginationResponse } from "@gitpod/public-api/lib/gitpod/v1/pagination_pb";

export class JsonRpcInstallationClient implements PromiseClient<typeof InstallationService> {
async listBlockedRepositories(
request: PartialMessage<ListBlockedRepositoriesRequest>,
_options?: CallOptions | undefined,
): Promise<ListBlockedRepositoriesResponse> {
// dashboard params is constant, no need to implement
const info = await getGitpodService().server.adminGetBlockedRepositories({
limit: 100,
offset: 0,
orderBy: "urlRegexp",
orderDir: "asc",
searchTerm: request.searchTerm,
});
return new ListBlockedRepositoriesResponse({
blockedRepositories: info.rows.map((item) => converter.toBlockedRepository(item)),
pagination: new PaginationResponse(),
});
}

async createBlockedRepository(
request: PartialMessage<CreateBlockedRepositoryRequest>,
_options?: CallOptions | undefined,
): Promise<CreateBlockedRepositoryResponse> {
if (!request.urlRegexp) {
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "urlRegexp is required");
}
if (request.blockUser === undefined) {
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "blockUser is required");
}
const info = await getGitpodService().server.adminCreateBlockedRepository(request.urlRegexp, request.blockUser);
return new CreateBlockedRepositoryResponse({
blockedRepository: converter.toBlockedRepository(info),
});
}

async deleteBlockedRepository(
request: PartialMessage<DeleteBlockedRepositoryRequest>,
_options?: CallOptions | undefined,
): Promise<DeleteBlockedRepositoryResponse> {
if (!request.blockedRepositoryId) {
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "blockedRepositoryId is required");
}
await getGitpodService().server.adminDeleteBlockedRepository(request.blockedRepositoryId);
return new DeleteBlockedRepositoryResponse();
}

async listBlockedEmailDomains(
request: PartialMessage<ListBlockedEmailDomainsRequest>,
_options?: CallOptions | undefined,
): Promise<ListBlockedEmailDomainsResponse> {
const info = await getGitpodService().server.adminGetBlockedEmailDomains();
return new ListBlockedEmailDomainsResponse({
blockedEmailDomains: info.map((item) => converter.toBlockedEmailDomain(item)),
pagination: new PaginationResponse(),
});
}

async createBlockedEmailDomain(
request: PartialMessage<CreateBlockedEmailDomainRequest>,
_options?: CallOptions | undefined,
): Promise<CreateBlockedEmailDomainResponse> {
if (!request.domain) {
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "domain is required");
}
if (request.negative === undefined) {
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "negative is required");
}
await getGitpodService().server.adminSaveBlockedEmailDomain({
domain: request.domain,
negative: request.negative,
});
// There's no way to get blockedEmailDomain, just ignore it since dashboard don't care about the response data
return new CreateBlockedEmailDomainResponse({});
}
}
8 changes: 8 additions & 0 deletions components/dashboard/src/service/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ import { SSHService } from "@gitpod/public-api/lib/gitpod/v1/ssh_connect";
import { JsonRpcSSHClient } from "./json-rpc-ssh-client";
import { JsonRpcVerificationClient } from "./json-rpc-verification-client";
import { VerificationService } from "@gitpod/public-api/lib/gitpod/v1/verification_connect";
import { JsonRpcInstallationClient } from "./json-rpc-installation-client";
import { InstallationService } from "@gitpod/public-api/lib/gitpod/v1/installation_connect";

const transport = createConnectTransport({
baseUrl: `${window.location.protocol}//${window.location.host}/public-api`,
Expand Down Expand Up @@ -66,6 +68,7 @@ export const organizationClient = createServiceClient(OrganizationService, {
client: new JsonRpcOrganizationClient(),
featureFlagSuffix: "organization",
});

// No jsonrcp client for the configuration service as it's only used in new UI of the dashboard
export const configurationClient = createServiceClient(ConfigurationService);
export const prebuildClient = createServiceClient(PrebuildService, {
Expand Down Expand Up @@ -98,6 +101,11 @@ export const verificationClient = createServiceClient(VerificationService, {
featureFlagSuffix: "verification",
});

export const installationClient = createServiceClient(InstallationService, {
client: new JsonRpcInstallationClient(),
featureFlagSuffix: "installation",
});

export async function listAllProjects(opts: { orgId: string }): Promise<ProtocolProject[]> {
let pagination = {
page: 1,
Expand Down
2 changes: 1 addition & 1 deletion components/gitpod-db/src/email-domain-filter-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { EmailDomainFilterEntry } from "@gitpod/gitpod-protocol";

export const EmailDomainFilterDB = Symbol("EmailDomainFilterDB");
export interface EmailDomainFilterDB {
storeFilterEntry(entry: EmailDomainFilterEntry): Promise<void>;
storeFilterEntry(entry: EmailDomainFilterEntry): Promise<EmailDomainFilterEntry>;
getFilterEntries(): Promise<EmailDomainFilterEntry[]>;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ export class EmailDomainFilterDBImpl implements EmailDomainFilterDB {
return (await this.getManager()).getRepository<DBEmailDomainFilterEntry>(DBEmailDomainFilterEntry);
}

async storeFilterEntry(entry: EmailDomainFilterEntry): Promise<void> {
async storeFilterEntry(entry: EmailDomainFilterEntry): Promise<EmailDomainFilterEntry> {
const repo = await this.getRepo();
await repo.save(entry);
return await repo.save(entry);
}

async getFilterEntries(): Promise<EmailDomainFilterEntry[]> {
Expand Down
Loading

0 comments on commit a997229

Please sign in to comment.