From 398ca46df47ac945b46c5c569c1528d37bf31e22 Mon Sep 17 00:00:00 2001 From: Bimal Gurung Date: Wed, 29 Jan 2025 20:51:54 +0545 Subject: [PATCH 1/6] feat(ui): add code refs feature --- ui/web-v2/package.json | 2 + ui/web-v2/src/assets/svg/bitbucket-icon.svg | 15 + ui/web-v2/src/assets/svg/github-icon.svg | 9 +- ui/web-v2/src/assets/svg/gitlab-icon.svg | 17 + .../src/components/AuditLogList/index.tsx | 2 + ui/web-v2/src/constants/routing.ts | 1 + ui/web-v2/src/grpc/codeRefs.ts | 123 ++++ ui/web-v2/src/modules/codeRefs.ts | 227 ++++++++ ui/web-v2/src/modules/index.ts | 2 + ui/web-v2/src/pages/feature/coderefs.tsx | 533 ++++++++++++++++++ ui/web-v2/src/pages/feature/detail.tsx | 9 + ui/web-v2/src/styles/styles.css | 1 + ui/web-v2/yarn.lock | 156 +++++ 13 files changed, 1093 insertions(+), 4 deletions(-) create mode 100644 ui/web-v2/src/assets/svg/bitbucket-icon.svg create mode 100644 ui/web-v2/src/assets/svg/gitlab-icon.svg create mode 100644 ui/web-v2/src/grpc/codeRefs.ts create mode 100644 ui/web-v2/src/modules/codeRefs.ts create mode 100644 ui/web-v2/src/pages/feature/coderefs.tsx diff --git a/ui/web-v2/package.json b/ui/web-v2/package.json index 36837ee0fd..66235d273c 100644 --- a/ui/web-v2/package.json +++ b/ui/web-v2/package.json @@ -54,6 +54,7 @@ "jstat": "1.9.4", "jwt-decode": "^4.0.0", "option-t": "23.0.2", + "prismjs": "^1.29.0", "prop-types": "^15.6.0", "query-string": "7.0.1", "react": "17.0.2", @@ -70,6 +71,7 @@ "react-redux": "7.2.6", "react-router-dom": "5.2.0", "react-select": "4.3.1", + "react-syntax-highlighter": "^15.6.1", "redux": "4.1.0", "redux-thunk": "2.3.0", "shallowequal": "1.1.0", diff --git a/ui/web-v2/src/assets/svg/bitbucket-icon.svg b/ui/web-v2/src/assets/svg/bitbucket-icon.svg new file mode 100644 index 0000000000..4e72756fcd --- /dev/null +++ b/ui/web-v2/src/assets/svg/bitbucket-icon.svg @@ -0,0 +1,15 @@ + + + + + + + + + + \ No newline at end of file diff --git a/ui/web-v2/src/assets/svg/github-icon.svg b/ui/web-v2/src/assets/svg/github-icon.svg index 40b4ccdf80..c453f3d18f 100644 --- a/ui/web-v2/src/assets/svg/github-icon.svg +++ b/ui/web-v2/src/assets/svg/github-icon.svg @@ -1,8 +1,9 @@ - + - + \ No newline at end of file diff --git a/ui/web-v2/src/assets/svg/gitlab-icon.svg b/ui/web-v2/src/assets/svg/gitlab-icon.svg new file mode 100644 index 0000000000..e3c2da500d --- /dev/null +++ b/ui/web-v2/src/assets/svg/gitlab-icon.svg @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file diff --git a/ui/web-v2/src/components/AuditLogList/index.tsx b/ui/web-v2/src/components/AuditLogList/index.tsx index a0e999052d..3e1c09ec3c 100644 --- a/ui/web-v2/src/components/AuditLogList/index.tsx +++ b/ui/web-v2/src/components/AuditLogList/index.tsx @@ -45,6 +45,8 @@ export const AuditLogList: FC = memo( shallowEqual ); + console.log({ auditLogs }); + return (
diff --git a/ui/web-v2/src/constants/routing.ts b/ui/web-v2/src/constants/routing.ts index 0ec581ed06..176adbb29a 100644 --- a/ui/web-v2/src/constants/routing.ts +++ b/ui/web-v2/src/constants/routing.ts @@ -28,6 +28,7 @@ export const PAGE_PATH_FEATURE_EVALUATION = '/evaluation'; export const PAGE_PATH_FEATURE_AUTOOPS = '/autoops'; export const PAGE_PATH_FEATURE_TRIGGER = '/trigger'; export const PAGE_PATH_FEATURE_HISTORY = '/history'; +export const PAGE_PATH_CODE_REFS = '/coderefs'; export const PAGE_PATH_AUTH_CALLBACK = '/auth/callback'; export const PAGE_PATH_AUTH_SIGNIN = '/auth/signin'; diff --git a/ui/web-v2/src/grpc/codeRefs.ts b/ui/web-v2/src/grpc/codeRefs.ts new file mode 100644 index 0000000000..1383fda361 --- /dev/null +++ b/ui/web-v2/src/grpc/codeRefs.ts @@ -0,0 +1,123 @@ +import { Nullable, isNotNull, isNull } from 'option-t/lib/Nullable/Nullable'; + +import { urls } from '../config'; +import { + ChangeAPIKeyNameRequest, + ChangeAPIKeyNameResponse, + CreateAPIKeyRequest, + CreateAPIKeyResponse, + DisableAPIKeyRequest, + DisableAPIKeyResponse, + EnableAPIKeyRequest, + EnableAPIKeyResponse, + GetAPIKeyRequest, + GetAPIKeyResponse, + ListAPIKeysRequest, + ListAPIKeysResponse +} from '../proto/account/service_pb'; +import { + AccountServiceClient, + ServiceError +} from '../proto/account/service_pb_service'; +import { CodeReferenceServiceClient } from '../proto/coderef/service_pb_service'; + +import { extractErrorMessage } from './messages'; +import { + checkUnauthenticatedError, + getMetaDataForClient as getMetaData +} from './utils'; +import { UNAUTHENTICATED_ERROR } from '../middlewares/thunkErrorHandler'; +import { + GetCodeReferenceRequest, + GetCodeReferenceResponse, + ListCodeReferencesRequest, + ListCodeReferencesResponse +} from '../proto/coderef/service_pb'; + +export class CodeRefsServiceError extends Error { + request: Request; + + error: Nullable; + + constructor( + message: string, + request: Request, + error: Nullable + ) { + if (checkUnauthenticatedError(error.code)) { + super(UNAUTHENTICATED_ERROR); + } else { + super(message); + } + if (Error.captureStackTrace) { + Error.captureStackTrace(this, CodeRefsServiceError); + } + this.name = 'CodeRefsServiceError'; + this.request = request; + this.error = error; + } +} + +const client = new CodeReferenceServiceClient(urls.GRPC); + +export interface GetCodeReferenceResult { + request: GetCodeReferenceRequest; + response: GetCodeReferenceResponse; +} + +export function getCodeRef( + request: GetCodeReferenceRequest +): Promise { + return new Promise( + (resolve: (result: GetCodeReferenceResult) => void, reject): void => { + client.getCodeReference( + request, + getMetaData(), + (error, response): void => { + if (isNotNull(error) || isNull(response)) { + reject( + new CodeRefsServiceError( + extractErrorMessage(error), + request, + error + ) + ); + } else { + resolve({ request, response }); + } + } + ); + } + ); +} + +export interface ListCodeRefsResult { + request: ListCodeReferencesRequest; + response: ListCodeReferencesResponse; +} + +export function listCodeRefs( + request: ListCodeReferencesRequest +): Promise { + return new Promise( + (resolve: (result: ListCodeRefsResult) => void, reject): void => { + client.listCodeReferences( + request, + getMetaData(), + (error, response): void => { + if (isNotNull(error) || isNull(response)) { + reject( + new CodeRefsServiceError( + extractErrorMessage(error), + request, + error + ) + ); + } else { + resolve({ request, response }); + } + } + ); + } + ); +} diff --git a/ui/web-v2/src/modules/codeRefs.ts b/ui/web-v2/src/modules/codeRefs.ts new file mode 100644 index 0000000000..d8617a24a4 --- /dev/null +++ b/ui/web-v2/src/modules/codeRefs.ts @@ -0,0 +1,227 @@ +import { + createSlice, + createEntityAdapter, + createAsyncThunk, + SerializedError +} from '@reduxjs/toolkit'; + +import * as grpc from '../grpc/codeRefs'; +import { APIKey } from '../proto/account/api_key_pb'; +import { CodeReference } from '../proto/coderef/code_reference_pb'; +import { + EnableAPIKeyCommand, + DisableAPIKeyCommand, + CreateAPIKeyCommand, + ChangeAPIKeyNameCommand +} from '../proto/account/command_pb'; +import { + ListAPIKeysRequest, + ListAPIKeysResponse, + GetAPIKeyRequest, + EnableAPIKeyRequest, + DisableAPIKeyRequest, + CreateAPIKeyRequest, + ChangeAPIKeyNameRequest +} from '../proto/account/service_pb'; + +import { AppState } from '.'; +import { + GetCodeReferenceRequest, + GetCodeReferenceResponse, + ListCodeReferencesRequest, + ListCodeReferencesResponse +} from '../proto/coderef/service_pb'; + +const MODULE_NAME = 'codeRefs'; + +export const codeRefsAdapter = createEntityAdapter({ + selectId: (codeRef: CodeReference.AsObject) => codeRef.id +}); + +export const { selectAll, selectById } = codeRefsAdapter.getSelectors(); + +interface ListCodeReferenceParams { + environmentId: string; + featureId: string; + pageSize: number; + fileExtension: string; + repositoryBranch: string; + repositoryType: CodeReference.RepositoryTypeMap[keyof CodeReference.RepositoryTypeMap]; +} + +export const listCodeRefs = createAsyncThunk< + ListCodeReferencesResponse.AsObject, + ListCodeReferenceParams | undefined, + { state: AppState } +>(`${MODULE_NAME}/list`, async (params) => { + const request = new ListCodeReferencesRequest(); + + console.log({ + params + }); + + request.setEnvironmentId(params.environmentId); + request.setFeatureId(params.featureId); + request.setPageSize(params.pageSize); + params.repositoryBranch && + request.setRepositoryBranch(params.repositoryBranch); + params.repositoryType && request.setRepositoryType(params.repositoryType); + params.fileExtension && request.setFileExtension(params.fileExtension); + + const result = await grpc.listCodeRefs(request); + return result.response.toObject(); +}); + +interface CodeReferenceParams { + environmentId: string; + id: string; +} + +export const getCodeRefs = createAsyncThunk< + GetCodeReferenceResponse.AsObject, + CodeReferenceParams | undefined, + { state: AppState } +>(`${MODULE_NAME}/get`, async (params) => { + const request = new GetCodeReferenceRequest(); + + request.setEnvironmentId(params.environmentId); + request.setId(params.id); + + const result = await grpc.getCodeRef(request); + + return result.response.toObject(); +}); + +export type OrderBy = + ListAPIKeysRequest.OrderByMap[keyof ListAPIKeysRequest.OrderByMap]; +export type OrderDirection = + ListAPIKeysRequest.OrderDirectionMap[keyof ListAPIKeysRequest.OrderDirectionMap]; + +export interface EnableAPIKeyParams { + environmentId: string; + id: string; +} + +export const enableAPIKey = createAsyncThunk< + void, + EnableAPIKeyParams | undefined, + { state: AppState } +>(`${MODULE_NAME}/enable`, async (params) => { + const request = new EnableAPIKeyRequest(); + request.setEnvironmentId(params.environmentId); + request.setId(params.id); + request.setCommand(new EnableAPIKeyCommand()); + // await grpc.enableAPIKey(request); +}); + +export interface DisableAPIKeyParams { + environmentId: string; + id: string; +} + +export const disableAPIKey = createAsyncThunk< + void, + DisableAPIKeyParams | undefined, + { state: AppState } +>(`${MODULE_NAME}/disable`, async (params) => { + const request = new DisableAPIKeyRequest(); + request.setEnvironmentId(params.environmentId); + request.setId(params.id); + request.setCommand(new DisableAPIKeyCommand()); + // await grpc.disableAPIKey(request); +}); + +export interface CreateAPIKeyParams { + environmentId: string; + name: string; + role: APIKey.RoleMap[keyof APIKey.RoleMap]; +} + +export const createAPIKey = createAsyncThunk< + void, + CreateAPIKeyParams | undefined, + { state: AppState } +>(`${MODULE_NAME}/add`, async (params) => { + const request = new CreateAPIKeyRequest(); + const cmd = new CreateAPIKeyCommand(); + cmd.setName(params.name); + cmd.setRole(params.role); + request.setEnvironmentId(params.environmentId); + request.setCommand(cmd); + // await grpc.createAPIKey(request); +}); + +export interface updateAPIKeyParams { + environmentId: string; + id: string; + name: string; +} + +export const updateAPIKey = createAsyncThunk< + void, + updateAPIKeyParams | undefined, + { state: AppState } +>(`${MODULE_NAME}/update`, async (params) => { + const request = new ChangeAPIKeyNameRequest(); + const cmd = new ChangeAPIKeyNameCommand(); + cmd.setName(params.name); + request.setEnvironmentId(params.environmentId); + request.setId(params.id); + request.setCommand(cmd); + // await grpc.changeAPIKeyName(request); +}); + +const initialState = codeRefsAdapter.getInitialState<{ + loading: boolean; + totalCount: number; + getAPIKeyError: SerializedError | null; +}>({ + loading: false, + totalCount: 0, + getAPIKeyError: null +}); + +export const codeRefsSlice = createSlice({ + name: MODULE_NAME, + initialState, + reducers: {}, + extraReducers: (builder) => { + builder + .addCase(listCodeRefs.pending, (state) => { + state.loading = true; + }) + .addCase(listCodeRefs.fulfilled, (state, action) => { + codeRefsAdapter.removeAll(state); + codeRefsAdapter.upsertMany(state, action.payload.codeReferencesList); + state.totalCount = action.payload.totalCount; + state.loading = false; + }) + .addCase(listCodeRefs.rejected, (state) => { + state.loading = false; + }) + // .addCase(getAPIKey.pending, (state) => { + // state.getAPIKeyError = null; + // }) + // .addCase(getAPIKey.fulfilled, (state, action) => { + // state.getAPIKeyError = null; + // if (action.payload) { + // codeRefsAdapter.upsertOne(state, action.payload); + // } + // }) + // .addCase(getAPIKey.rejected, (state, action) => { + // state.getAPIKeyError = action.error; + // }) + .addCase(enableAPIKey.pending, () => {}) + .addCase(enableAPIKey.fulfilled, () => {}) + .addCase(enableAPIKey.rejected, () => {}) + .addCase(disableAPIKey.pending, () => {}) + .addCase(disableAPIKey.fulfilled, () => {}) + .addCase(disableAPIKey.rejected, () => {}) + .addCase(createAPIKey.pending, () => {}) + .addCase(createAPIKey.fulfilled, () => {}) + .addCase(createAPIKey.rejected, () => {}) + .addCase(updateAPIKey.pending, () => {}) + .addCase(updateAPIKey.fulfilled, () => {}) + .addCase(updateAPIKey.rejected, () => {}); + } +}); diff --git a/ui/web-v2/src/modules/index.ts b/ui/web-v2/src/modules/index.ts index f25c693651..05eb12b574 100644 --- a/ui/web-v2/src/modules/index.ts +++ b/ui/web-v2/src/modules/index.ts @@ -4,6 +4,7 @@ import { ThunkAction } from 'redux-thunk'; import { accountsSlice } from './accounts'; import { adminNotificationSlice } from './adminNotifications'; import { apiKeySlice } from './apiKeys'; +import { codeRefsSlice } from './codeRefs'; import { auditLogSlice } from './auditLogs'; import { authSlice } from './auth'; import { autoOpsRulesSlice } from './autoOpsRules'; @@ -30,6 +31,7 @@ export const reducers = combineReducers({ adminNotification: adminNotificationSlice.reducer, auditLog: auditLogSlice.reducer, apiKeys: apiKeySlice.reducer, + codeRefs: codeRefsSlice.reducer, auth: authSlice.reducer, autoOpsRules: autoOpsRulesSlice.reducer, progressiveRollout: progressiveRolloutSlice.reducer, diff --git a/ui/web-v2/src/pages/feature/coderefs.tsx b/ui/web-v2/src/pages/feature/coderefs.tsx new file mode 100644 index 0000000000..1a9c286f42 --- /dev/null +++ b/ui/web-v2/src/pages/feature/coderefs.tsx @@ -0,0 +1,533 @@ +import React, { FC, memo, useEffect, useRef, useState } from 'react'; +import { shallowEqual, useDispatch, useSelector } from 'react-redux'; +import { useHistory } from 'react-router-dom'; +import { useCurrentEnvironment } from '../../modules/me'; +import { AppDispatch } from '../../store'; +import { listAPIKeys } from '../../modules/apiKeys'; +import { getOrganizationId } from '../../storage/organizationId'; +import { APIKEY_LIST_PAGE_SIZE } from '../../constants/apiKey'; +import { + listCodeRefs, + selectAll as selectAllCodeRefs +} from '../../modules/codeRefs'; +import { APIKey } from '../../proto/account/api_key_pb'; +import { ListAPIKeysResponse } from '../../proto/account/service_pb'; +import { ListCodeReferencesRequest } from '../../proto/coderef/service_pb'; +import { AppState } from '../../modules'; +import { PAGE_PATH_APIKEYS, PAGE_PATH_ROOT } from '../../constants/routing'; +import { DetailSkeleton } from '../../components/DetailSkeleton'; +import { CodeReference } from '../../proto/coderef/code_reference_pb'; +import { classNames } from '../../utils/css'; +import { Option, Select } from '../../components/Select'; +import { components } from 'react-select'; +import GithubIcon from '../../assets/svg/github-icon.svg'; +import GitlabIcon from '../../assets/svg/gitlab-icon.svg'; +import BitbucketIcon from '../../assets/svg/bitbucket-icon.svg'; +import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/outline'; +import { useIntl } from 'react-intl'; +import { messages } from '../../lang/messages'; +import { ListSkeleton } from '../../components/ListSkeleton'; +import SyntaxHighlighter from 'react-syntax-highlighter'; +import { docco } from 'react-syntax-highlighter/dist/esm/styles/hljs'; + +const repositoryOptions = [ + { + label: 'All', + value: CodeReference.RepositoryType.REPOSITORY_TYPE_UNSPECIFIED.toString() + }, + { + label: 'GitHub', + value: CodeReference.RepositoryType.GITHUB.toString() + }, + { + label: 'GitLab', + value: CodeReference.RepositoryType.GITLAB.toString() + }, + { + label: 'Bitbucket', + value: CodeReference.RepositoryType.BITBUCKET.toString() + } +]; + +const fileExtensionOptions = [ + { label: 'All', value: null }, + { label: '.js', value: '.js' }, + { label: '.jsx', value: '.jsx' }, + { label: '.ts', value: '.ts' } +]; + +interface FeatureCodeRefsPageProps { + featureId: string; +} + +export const FeatureCodeRefsPage: FC = memo( + ({ featureId }) => { + const dispatch = useDispatch(); + const currentEnvironment = useCurrentEnvironment(); + const history = useHistory(); + const { formatMessage: f } = useIntl(); + + const [isLoading, setIsLoading] = React.useState(true); + const [hasValidApiKey, setHasValidApiKey] = React.useState(false); + const [selectedRepository, setSelectedRepository] = useState