diff --git a/package.json b/package.json index d4615ed427..76b06b35c7 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "homepage": "/dashboard", "dependencies": { - "@devtron-labs/devtron-fe-common-lib": "1.17.0-pre-12", + "@devtron-labs/devtron-fe-common-lib": "1.17.0-pre-13", "@esbuild-plugins/node-globals-polyfill": "0.2.3", "@rjsf/core": "^5.13.3", "@rjsf/utils": "^5.13.3", diff --git a/src/Pages/GlobalConfigurations/Authorization/APITokens/ApiTokens.component.tsx b/src/Pages/GlobalConfigurations/Authorization/APITokens/ApiTokens.component.tsx index 1634d19dd4..7b425b5acc 100644 --- a/src/Pages/GlobalConfigurations/Authorization/APITokens/ApiTokens.component.tsx +++ b/src/Pages/GlobalConfigurations/Authorization/APITokens/ApiTokens.component.tsx @@ -28,7 +28,7 @@ import { import emptyGeneratToken from '@Images/ic-empty-generate-token.png' import { EMPTY_STATE_STATUS } from '@Config/constantMessaging' -import { TokenListType, TokenResponseType } from './apiToken.type' +import { TokenListType } from './apiToken.type' import APITokenList from './APITokenList' import CreateAPIToken from './CreateAPIToken' import EditAPIToken from './EditAPIToken' @@ -90,13 +90,6 @@ const ApiTokens = () => { handleFilterChanges(_searchText) } - const [tokenResponse, setTokenResponse] = useState({ - success: false, - token: '', - userId: 0, - userIdentifier: 'API-TOKEN:test', - }) - const renderSearchToken = () => ( { handleGenerateTokenActionButton={handleActionButton} setSelectedExpirationDate={setSelectedExpirationDate} selectedExpirationDate={selectedExpirationDate} - tokenResponse={tokenResponse} - setTokenResponse={setTokenResponse} reload={getData} /> diff --git a/src/Pages/GlobalConfigurations/Authorization/APITokens/CreateAPIToken.tsx b/src/Pages/GlobalConfigurations/Authorization/APITokens/CreateAPIToken.tsx index 1e50563a29..973fed3f23 100644 --- a/src/Pages/GlobalConfigurations/Authorization/APITokens/CreateAPIToken.tsx +++ b/src/Pages/GlobalConfigurations/Authorization/APITokens/CreateAPIToken.tsx @@ -43,7 +43,7 @@ import { usePermissionConfiguration, } from '../Shared/components/PermissionConfigurationForm' import { createUserPermissionPayload, validateDirectPermissionForm } from '../utils' -import { FormType, GenerateTokenType } from './apiToken.type' +import { FormType, GenerateTokenType, TokenResponseType } from './apiToken.type' import { getDateInMilliseconds } from './apiToken.utils' import ExpirationDate from './ExpirationDate' import GenerateActionButton from './GenerateActionButton' @@ -69,8 +69,6 @@ const CreateAPIToken = ({ handleGenerateTokenActionButton, setSelectedExpirationDate, selectedExpirationDate, - tokenResponse, - setTokenResponse, reload, }: GenerateTokenType) => { const history = useHistory() @@ -106,6 +104,13 @@ const CreateAPIToken = ({ allowManageAllAccess, } = usePermissionConfiguration() const [customDate, setCustomDate] = useState(null) + const [tokenResponse, setTokenResponse] = useState({ + success: false, + token: '', + userId: 0, + userIdentifier: 'API-TOKEN:test', + hideApiToken: false, + }) const validationRules = new ValidationRules() // Reset selected expiration date to 30 days on unmount @@ -210,6 +215,7 @@ const CreateAPIToken = ({ const userPermissionPayload = createUserPermissionPayload({ id: result.userId, userIdentifier: result.userIdentifier, + hideApiToken: result.hideApiToken, userRoleGroups, serverMode, directPermission, @@ -308,13 +314,13 @@ const CreateAPIToken = ({ buttonText="Generate token" disabled={isSaveDisabled} /> - ) diff --git a/src/Pages/GlobalConfigurations/Authorization/APITokens/EditAPIToken.tsx b/src/Pages/GlobalConfigurations/Authorization/APITokens/EditAPIToken.tsx index f9ef712328..9ccaa362e5 100644 --- a/src/Pages/GlobalConfigurations/Authorization/APITokens/EditAPIToken.tsx +++ b/src/Pages/GlobalConfigurations/Authorization/APITokens/EditAPIToken.tsx @@ -23,6 +23,7 @@ import { Button, ButtonStyleType, ButtonVariantType, + ClipboardButton, CustomInput, Icon, InfoBlock, @@ -243,6 +244,19 @@ const EditAPIToken = ({ placeholder="Enter a description to remember where you have used this token" error={invalidDescription ? 'Max 350 characters allowed.' : null} /> + {!!editData?.token?.length && ( + + )}
diff --git a/src/Pages/GlobalConfigurations/Authorization/APITokens/GenerateModal.tsx b/src/Pages/GlobalConfigurations/Authorization/APITokens/GenerateModal.tsx index ca5f6e7c08..af61b6873b 100644 --- a/src/Pages/GlobalConfigurations/Authorization/APITokens/GenerateModal.tsx +++ b/src/Pages/GlobalConfigurations/Authorization/APITokens/GenerateModal.tsx @@ -26,14 +26,16 @@ import { } from '@devtron-labs/devtron-fe-common-lib' import { GenerateTokenModalType } from './apiToken.type' +import { getApiTokenHeader } from './apiToken.utils' const GenerateModal = ({ close, - token, + token = '', reload, redirectToTokenList, isRegenerationModal, open, + hideApiToken = false, }: GenerateTokenModalType) => { const [copyToClipboardPromise, setCopyToClipboardPromise] = useState>(null) const modelType = isRegenerationModal ? 'Regenerated' : 'Generated' @@ -60,9 +62,7 @@ const GenerateModal = ({
-
- Copy and store this token safely, you won’t be able to view it again. -
+
{getApiTokenHeader(hideApiToken)}

You can regenerate a token anytime. If you do, remember to update any scripts or applications using the old token. @@ -71,7 +71,11 @@ const GenerateModal = ({ + {token} +

+ } variant="success" customIcon={} /> diff --git a/src/Pages/GlobalConfigurations/Authorization/APITokens/RegenerateModal.tsx b/src/Pages/GlobalConfigurations/Authorization/APITokens/RegenerateModal.tsx index 8b57bba58a..7993086765 100644 --- a/src/Pages/GlobalConfigurations/Authorization/APITokens/RegenerateModal.tsx +++ b/src/Pages/GlobalConfigurations/Authorization/APITokens/RegenerateModal.tsx @@ -117,6 +117,7 @@ const RegeneratedModal = ({ redirectToTokenList={redirectToTokenList} isRegenerationModal open={showGenerateModal} + hideApiToken={tokenResponse.hideApiToken} /> ) : ( diff --git a/src/Pages/GlobalConfigurations/Authorization/APITokens/apiToken.type.ts b/src/Pages/GlobalConfigurations/Authorization/APITokens/apiToken.type.ts index 7eec491f56..96ad3c6403 100644 --- a/src/Pages/GlobalConfigurations/Authorization/APITokens/apiToken.type.ts +++ b/src/Pages/GlobalConfigurations/Authorization/APITokens/apiToken.type.ts @@ -26,9 +26,10 @@ export interface FormType { } export interface TokenResponseType { success: boolean - token: string userId: number userIdentifier: string + hideApiToken: boolean + token?: string } export interface GenerateTokenType { @@ -37,17 +38,13 @@ export interface GenerateTokenType { handleGenerateTokenActionButton: () => void setSelectedExpirationDate selectedExpirationDate - tokenResponse: TokenResponseType - setTokenResponse: React.Dispatch> reload: () => void } -export interface TokenListType { +export interface TokenListType extends Pick { expireAtInMs: number id: number name: string - userId: number - userIdentifier: string description: string lastUsedByIp?: string lastUsedAt?: string @@ -55,7 +52,10 @@ export interface TokenListType { } export interface EditDataType - extends Pick {} + extends Pick< + TokenListType, + 'name' | 'description' | 'expireAtInMs' | 'token' | 'id' | 'userId' | 'userIdentifier' | 'hideApiToken' + > {} export interface EditTokenType { setShowRegeneratedModal: React.Dispatch> showRegeneratedModal: boolean @@ -77,11 +77,12 @@ export interface GenerateActionButtonType { export interface GenerateTokenModalType { close: () => void - token: string + token: TokenListType['token'] reload: () => void redirectToTokenList: () => void isRegenerationModal?: boolean open: GenericModalProps['open'] + hideApiToken: TokenListType['hideApiToken'] } export interface APITokenListType { diff --git a/src/Pages/GlobalConfigurations/Authorization/APITokens/apiToken.utils.ts b/src/Pages/GlobalConfigurations/Authorization/APITokens/apiToken.utils.ts index bb6a153ea8..b0ede7d037 100644 --- a/src/Pages/GlobalConfigurations/Authorization/APITokens/apiToken.utils.ts +++ b/src/Pages/GlobalConfigurations/Authorization/APITokens/apiToken.utils.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { TokenListType } from './apiToken.type' + export function getOptions(customDate) { return [ { value: 7, label: '7 days' }, @@ -36,3 +38,6 @@ export const isTokenExpired = (expiredDate: number): boolean => { return getDateInMilliseconds(new Date().valueOf()) > getDateInMilliseconds(expiredDate) } + +export const getApiTokenHeader = (hideApiToken: TokenListType['hideApiToken']) => + `Copy and store this token safely ${hideApiToken ? ', you won’t be able to view it again.' : ''}` diff --git a/src/Pages/GlobalConfigurations/Authorization/types.ts b/src/Pages/GlobalConfigurations/Authorization/types.ts index b548c938c3..730a16421b 100644 --- a/src/Pages/GlobalConfigurations/Authorization/types.ts +++ b/src/Pages/GlobalConfigurations/Authorization/types.ts @@ -37,6 +37,7 @@ import { } from '@devtron-labs/devtron-fe-common-lib' import { SERVER_MODE } from '../../../config' +import { TokenResponseType } from './APITokens/apiToken.type' import { PermissionType, UserRoleType } from './constants' export interface UserAndGroupPermissionsWrapProps { @@ -311,7 +312,8 @@ export interface CreateUserPermissionPayloadParams extends Pick { id: number userGroups: Pick[] - userIdentifier: string + hideApiToken?: TokenResponseType['hideApiToken'] + userIdentifier: TokenResponseType['userIdentifier'] serverMode: SERVER_MODE directPermission: DirectPermissionsRoleFilter[] chartPermission: ChartGroupPermissionsFilter diff --git a/src/components/ciPipeline/Webhook/WebhookDetailsModal.tsx b/src/components/ciPipeline/Webhook/WebhookDetailsModal.tsx index 2c8e52e8f0..68b0b5667e 100644 --- a/src/components/ciPipeline/Webhook/WebhookDetailsModal.tsx +++ b/src/components/ciPipeline/Webhook/WebhookDetailsModal.tsx @@ -38,6 +38,7 @@ import { stopPropagation, ButtonStyleType, Icon, + SelectPicker, } from '@devtron-labs/devtron-fe-common-lib' import { useParams } from 'react-router-dom' import Tippy from '@tippyjs/react' @@ -56,9 +57,11 @@ import { PLAYGROUND_TAB_LIST, REQUEST_BODY_TAB_LIST, RESPONSE_TAB_LIST, + SELECT_AUTO_GENERATE_TOKEN_WITH_REQUIRED_PERMISSIONS, + TOKEN_TAB_LIST, } from './webhook.utils' -import { SchemaType, TabDetailsType, WebhookDetailsType, WebhookDetailType } from './types' -import { executeWebhookAPI, getExternalCIConfig } from './webhook.service' +import { SchemaType, TabDetailsType, TokenListOptionsType, WebhookDetailsType, WebhookDetailType } from './types' +import { executeWebhookAPI, getExternalCIConfig, getWebhookAPITokenList } from './webhook.service' import { GENERATE_TOKEN_NAME_VALIDATION } from '../../../config/constantMessaging' import { createUserPermissionPayload } from '@Pages/GlobalConfigurations/Authorization/utils' import { ChartGroupPermissionsFilter } from '@Pages/GlobalConfigurations/Authorization/types' @@ -67,6 +70,7 @@ import { getDefaultStatusAndTimeout, getDefaultUserStatusAndTimeout, } from '@Pages/GlobalConfigurations/Authorization/libUtils' +import { getApiTokenHeader } from '@Pages/GlobalConfigurations/Authorization/APITokens/apiToken.utils' export const WebhookDetailsModal = ({ close, isTemplateView }: WebhookDetailType) => { const { appId, webhookId } = useParams<{ @@ -84,6 +88,7 @@ export const WebhookDetailsModal = ({ close, isTemplateView }: WebhookDetailType const [selectedRequestBodyTab, setRequestBodyPlaygroundTab] = useState(REQUEST_BODY_TAB_LIST[0].key) const [webhookResponse, setWebhookResponse] = useState(null) const [generatedAPIToken, setGeneratedAPIToken] = useState(null) + const [selectedTokenTab, setSelectedTokenTab] = useState(TOKEN_TAB_LIST[0].key) const [showTokenSection, setShowTokenSection] = useState(false) const [isSuperAdmin, setIsSuperAdmin] = useState(false) const [samplePayload, setSamplePayload] = useState(null) @@ -94,6 +99,8 @@ export const WebhookDetailsModal = ({ close, isTemplateView }: WebhookDetailType const [tryoutAPIToken, setTryoutAPIToken] = useState(null) const [showTryoutAPITokenError, setTryoutAPITokenError] = useState(false) const [webhookDetails, setWebhookDetails] = useState(null) + const [selectedToken, setSelectedToken] = useState(null) + const [tokenList, setTokenList] = useState(null) const [selectedSchema, setSelectedSchema] = useState('') const [errorInGetData, setErrorInGetData] = useState(false) const [copyToClipboardPromise, setCopyToClipboardPromise] = useState>(null) @@ -184,6 +191,23 @@ export const WebhookDetailsModal = ({ close, isTemplateView }: WebhookDetailType setSampleCURL( CURL_PREFIX.replace('{webhookURL}', _webhookDetails.webhookUrl).replace('{data}', modifiedJSONString), ) + if (_isSuperAdmin) { + const { result } = await getWebhookAPITokenList( + _webhookDetails.projectName, + _webhookDetails.environmentIdentifier, + _webhookDetails.appName, + ) + const sortedResult = + result + ?.sort((a, b) => a['name'].localeCompare(b['name'])) + .map((tokenData) => ({ + ...tokenData, + label: tokenData.name, + value: tokenData.id.toString(), + description: 'Has access', + })) || [] + setTokenList(sortedResult) + } setLoader(false) } catch (error) { setIsSuperAdmin(false) @@ -191,6 +215,9 @@ export const WebhookDetailsModal = ({ close, isTemplateView }: WebhookDetailType setErrorInGetData(true) } } + + const hideApiToken = !tokenList?.[0]?.token + const generateToken = async (): Promise => { if (!tokenName) { setTokenNameError(true) @@ -208,6 +235,7 @@ export const WebhookDetailsModal = ({ close, isTemplateView }: WebhookDetailType const userPermissionPayload = createUserPermissionPayload({ id: result.userId, userIdentifier: result.userIdentifier, + hideApiToken: result.hideApiToken, userRoleGroups: [], serverMode: SERVER_MODE.FULL, directPermission: [], @@ -367,7 +395,7 @@ export const WebhookDetailsModal = ({ close, isTemplateView }: WebhookDetailType return (
{token} @@ -424,6 +452,47 @@ export const WebhookDetailsModal = ({ close, isTemplateView }: WebhookDetailType ) } + const handleSelectedTokenChange = (selectedToken): void => { + setSelectedToken(selectedToken) + } + + const renderSelectTokenSection = (): JSX.Element => ( + <> +
+ +
+ {selectedToken?.name && renderSelectedToken(selectedToken.token)} + + ) + + const renderGeneratedTokenDetails = () => { + if (hideApiToken) { + return ( +
+ {GENERATE_TOKEN_WITH_REQUIRED_PERMISSIONS} + {renderGenerateTokenSection()} +
+ ) + } + return ( +
+ {generateTabHeader(TOKEN_TAB_LIST, selectedTokenTab, setSelectedTokenTab)} + {selectedTokenTab === TOKEN_TAB_LIST[0].key && renderSelectTokenSection()} + {selectedTokenTab === TOKEN_TAB_LIST[1].key && renderGenerateTokenSection()} +
+ ) + } + const renderTokenSection = (): JSX.Element | null => { if (!isSuperAdmin) { return ( @@ -440,13 +509,14 @@ export const WebhookDetailsModal = ({ close, isTemplateView }: WebhookDetailType dataTestId="select-or-generate-token" variant={ButtonVariantType.text} onClick={toggleTokenSection} - text={GENERATE_TOKEN_WITH_REQUIRED_PERMISSIONS} + text={ + hideApiToken + ? GENERATE_TOKEN_WITH_REQUIRED_PERMISSIONS + : SELECT_AUTO_GENERATE_TOKEN_WITH_REQUIRED_PERMISSIONS + } /> ) : ( -
- {GENERATE_TOKEN_WITH_REQUIRED_PERMISSIONS} - {renderGenerateTokenSection()} -
+ renderGeneratedTokenDetails() ) } diff --git a/src/components/ciPipeline/Webhook/types.ts b/src/components/ciPipeline/Webhook/types.ts index 50422e73f7..9faf1ff987 100644 --- a/src/components/ciPipeline/Webhook/types.ts +++ b/src/components/ciPipeline/Webhook/types.ts @@ -14,12 +14,18 @@ * limitations under the License. */ -import { AppConfigProps, ResponseType } from '@devtron-labs/devtron-fe-common-lib' +import { AppConfigProps, ResponseType, SelectPickerOptionType } from '@devtron-labs/devtron-fe-common-lib' + +import { TokenListType } from '@Pages/GlobalConfigurations/Authorization/APITokens/apiToken.type' export interface WebhookDetailType extends Required> { close: () => void } +export interface TokenListOptionsType extends TokenListType, SelectPickerOptionType { + description: string +} + export interface TabDetailsType { key: string value: string @@ -85,3 +91,5 @@ export interface WebhookDetailsResponse extends ResponseType { export interface WebhookListResponse extends ResponseType { result?: WebhookDetailsType[] } + +export type WebhookApiTokenResponse = ResponseType diff --git a/src/components/ciPipeline/Webhook/webhook.service.ts b/src/components/ciPipeline/Webhook/webhook.service.ts index 05b56715f3..0741f1a2fa 100644 --- a/src/components/ciPipeline/Webhook/webhook.service.ts +++ b/src/components/ciPipeline/Webhook/webhook.service.ts @@ -19,17 +19,23 @@ import { get, GetTemplateAPIRouteType, getTemplateAPIRoute, + getUrlWithSearchParams, } from '@devtron-labs/devtron-fe-common-lib' import { Routes } from '../../../config' -import { WebhookDetailsResponse, WebhookListResponse } from './types' +import { WebhookApiTokenResponse, WebhookDetailsResponse, WebhookListResponse } from './types' -export function getExternalCIList(appId: number | string, isTemplateView: AppConfigProps['isTemplateView']): Promise { - const url = isTemplateView ? getTemplateAPIRoute({ - type: GetTemplateAPIRouteType.EXTERNAL_CI_LIST, - queryParams: { - id: appId, - } - }) : `${Routes.EXTERNAL_CI_CONFIG}/${appId}` +export function getExternalCIList( + appId: number | string, + isTemplateView: AppConfigProps['isTemplateView'], +): Promise { + const url = isTemplateView + ? getTemplateAPIRoute({ + type: GetTemplateAPIRouteType.EXTERNAL_CI_LIST, + queryParams: { + id: appId, + }, + }) + : `${Routes.EXTERNAL_CI_CONFIG}/${appId}` return get(url) } @@ -52,6 +58,20 @@ export function getExternalCIConfig( return get(url) } +export function getWebhookAPITokenList( + projectName: string, + environmentName: string, + appName: string, +): Promise { + return get( + getUrlWithSearchParams(Routes.API_TOKEN_WEBHOOK, { + projectName: projectName, + environmentName: environmentName, + appName: appName, + }), + ) +} + export async function executeWebhookAPI(webhookUrl: string, token: string, data?: object): Promise { const options = { method: 'POST', diff --git a/src/components/ciPipeline/Webhook/webhook.utils.ts b/src/components/ciPipeline/Webhook/webhook.utils.ts index 25eb584774..43af1afb67 100644 --- a/src/components/ciPipeline/Webhook/webhook.utils.ts +++ b/src/components/ciPipeline/Webhook/webhook.utils.ts @@ -16,6 +16,11 @@ import { TabDetailsType } from './types' +export const TOKEN_TAB_LIST: TabDetailsType[] = [ + { key: 'selectToken', value: 'Select API token' }, + { key: 'autoToken', value: 'Auto-generate token' }, +] + export const PLAYGROUND_TAB_LIST: TabDetailsType[] = [ { key: 'webhookURL', value: 'Webhook URL' }, { key: 'sampleCurl', value: 'Sample cURL request' }, @@ -37,3 +42,6 @@ export const CURL_PREFIX = `curl --location --request POST \\ --data-raw '{data}'` export const GENERATE_TOKEN_WITH_REQUIRED_PERMISSIONS = 'Generate token with required permissions' +export const SELECT_AUTO_GENERATE_TOKEN_WITH_REQUIRED_PERMISSIONS = + 'Select or auto-generate token with required permissions' + diff --git a/yarn.lock b/yarn.lock index 0d445af28d..5000eb6448 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1722,9 +1722,9 @@ __metadata: languageName: node linkType: hard -"@devtron-labs/devtron-fe-common-lib@npm:1.17.0-pre-12": - version: 1.17.0-pre-12 - resolution: "@devtron-labs/devtron-fe-common-lib@npm:1.17.0-pre-12" +"@devtron-labs/devtron-fe-common-lib@npm:1.17.0-pre-13": + version: 1.17.0-pre-13 + resolution: "@devtron-labs/devtron-fe-common-lib@npm:1.17.0-pre-13" dependencies: "@codemirror/autocomplete": "npm:6.18.6" "@codemirror/lang-json": "npm:6.0.1" @@ -1773,7 +1773,7 @@ __metadata: react-select: 5.8.0 rxjs: ^7.8.1 yaml: ^2.4.1 - checksum: 10c0/dc0fc8dd913203bc72c7246ebb46a186c55cce03bb6064425c618ee22ccaa432b6671e348a3b2797f0188464cf17eeaf2d082131f3a7dbe9330570bf3a1145d9 + checksum: 10c0/1e30a7ea88dd09be5fbcc140edea8d24495bd90b945ebcd0dc41cdebe69a8e2b41770632e3741f1833c26d5d5b088c18ce2cd2e48fdf0e88ff617d3e9383f52b languageName: node linkType: hard @@ -5685,7 +5685,7 @@ __metadata: version: 0.0.0-use.local resolution: "dashboard@workspace:." dependencies: - "@devtron-labs/devtron-fe-common-lib": "npm:1.17.0-pre-12" + "@devtron-labs/devtron-fe-common-lib": "npm:1.17.0-pre-13" "@esbuild-plugins/node-globals-polyfill": "npm:0.2.3" "@playwright/test": "npm:^1.32.1" "@rjsf/core": "npm:^5.13.3"