Skip to content

Commit de7a5c1

Browse files
committed
hide api key
1 parent 2019dcb commit de7a5c1

File tree

4 files changed

+96
-24
lines changed

4 files changed

+96
-24
lines changed

packages/cypress/cypress/pages/modelsAsAService.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -614,7 +614,7 @@ class CopyApiKeyModal extends Modal {
614614

615615
find(): Cypress.Chainable<JQuery<HTMLElement>> {
616616
// Find the dialog that contains the API key token copy (unique to this modal)
617-
return cy.findByTestId('api-key-token-copy').closest('[role="dialog"]');
617+
return cy.findByTestId('api-key-token-copy-section').closest('[role="dialog"]');
618618
}
619619

620620
shouldBeOpen(open = true): void {
@@ -626,24 +626,28 @@ class CopyApiKeyModal extends Modal {
626626
}
627627

628628
findApiKeyTokenCopy(): Cypress.Chainable<JQuery<HTMLElement>> {
629-
return cy.findByTestId('api-key-token-copy');
629+
return this.find().findByTestId('api-key-token-copy-section');
630630
}
631631

632632
findApiKeyTokenCopyButton(): Cypress.Chainable<JQuery<HTMLElement>> {
633-
return this.findApiKeyTokenCopy().findByRole('button', { name: 'Copy' });
633+
return this.find().findByTestId('api-key-token-copy-button');
634634
}
635635

636-
findApiKeyTokenInput(): Cypress.Chainable<JQuery<HTMLInputElement>> {
637-
// Find the read-only input field inside the ClipboardCopy component
638-
return this.findApiKeyTokenCopy().find('input[type="text"]');
636+
findApiKeyTokenInput(): Cypress.Chainable<JQuery<HTMLElement>> {
637+
// input/textarea holds the value; PF wraps TextInput in a span, value on the span is undefined).
638+
return this.find().find('input[aria-label="API key"], textarea[aria-label="API key"]');
639+
}
640+
641+
findApiKeyTokenVisibilityToggle(): Cypress.Chainable<JQuery<HTMLElement>> {
642+
return this.find().findByTestId('api-key-visibility-toggle');
639643
}
640644

641645
findApiKeyName(): Cypress.Chainable<JQuery<HTMLElement>> {
642-
return cy.findByTestId('api-key-display-name');
646+
return this.find().findByTestId('api-key-display-name');
643647
}
644648

645649
findApiKeyExpirationDate(): Cypress.Chainable<JQuery<HTMLElement>> {
646-
return cy.findByTestId('api-key-display-expiration');
650+
return this.find().findByTestId('api-key-display-expiration');
647651
}
648652
}
649653

packages/cypress/cypress/tests/mocked/modelsAsAService/maasApiKeys.cy.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -427,11 +427,48 @@ describe('API Keys Page', () => {
427427
});
428428

429429
copyApiKeyModal.shouldBeOpen();
430-
copyApiKeyModal.findApiKeyTokenInput().should('have.value', mockCreateAPIKeyResponse().key);
431430
copyApiKeyModal.findApiKeyName().should('contain.text', 'production-backend');
432431
copyApiKeyModal.findApiKeyExpirationDate().should('contain.text', '30 days');
433432
});
434433

434+
it('should show/hide the token when the visibility toggle is clicked', () => {
435+
cy.interceptOdh('POST /maas/api/v1/api-keys', {
436+
data: mockCreateAPIKeyResponse(),
437+
}).as('createApiKey');
438+
439+
apiKeysPage.findCreateApiKeyButton().click();
440+
createApiKeyModal.shouldBeOpen();
441+
cy.wait('@getSubscriptions');
442+
createApiKeyModal.findExpirationToggle().should('contain.text', '30 days');
443+
createApiKeyModal.findSubmitButton().should('be.disabled');
444+
createApiKeyModal.findSubscriptionToggle().click();
445+
createApiKeyModal.findSubscriptionOption('premium-team-sub').click();
446+
createApiKeyModal.findNameInput().type('production-backend');
447+
createApiKeyModal.findDescriptionInput().type('Production API key for backend service');
448+
createApiKeyModal.findSubmitButton().should('be.enabled');
449+
createApiKeyModal.findSubmitButton().click();
450+
cy.wait('@createApiKey').then((interception) => {
451+
expect(interception.request.body?.data).to.include({ expiresIn: '30d' });
452+
expect(interception.response?.body?.data).to.include({
453+
name: 'production-backend',
454+
expiresAt: '2026-01-20T11:54:34.521671447-05:00',
455+
});
456+
});
457+
copyApiKeyModal.shouldBeOpen();
458+
copyApiKeyModal.findApiKeyTokenInput().should('have.value', '•'.repeat(40));
459+
copyApiKeyModal.findApiKeyTokenVisibilityToggle().should('be.visible').click();
460+
copyApiKeyModal.findApiKeyTokenInput().should('have.value', mockCreateAPIKeyResponse().key);
461+
462+
cy.window().then((win) => {
463+
cy.stub(win.navigator.clipboard, 'writeText').as('clipboardWrite');
464+
});
465+
copyApiKeyModal.findApiKeyTokenCopyButton().click();
466+
cy.get('@clipboardWrite').should('have.been.calledOnce');
467+
cy.get('@clipboardWrite').should('have.been.calledWith', mockCreateAPIKeyResponse().key);
468+
copyApiKeyModal.findApiKeyTokenVisibilityToggle().should('be.visible').click();
469+
copyApiKeyModal.findApiKeyTokenInput().should('have.value', '•'.repeat(40));
470+
});
471+
435472
it('should show the custom days input when Custom (days) is selected and hide it when switching back', () => {
436473
apiKeysPage.findCreateApiKeyButton().click();
437474
createApiKeyModal.shouldBeOpen();

packages/cypress/cypress/utils/maasUtils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export const mockAPIKeys = (): APIKey[] => [
6060

6161
export const mockCreateAPIKeyResponse = (): CreateAPIKeyResponse => {
6262
return {
63-
key: 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJtYWFzLWFwaSIsInN1YiI6InRlc3QtdXNlciIsImF1ZCI6WyJtYWFzLWFwaSJdLCJleHAiOjE2NzI1NDU2MDAsIm5iZiI6MTY3MjUzMTIwMCwiaWF0IjoxNjcyNTMxMjAwfQ.mock-signature',
63+
key: 'sk-oai-1JO088RHrhLvlwNqT_LDEQgy7IbnbyoSYQCjuMqLpzRI8xns9gBFo0bZsaSat',
6464
keyPrefix: 'sk-oai-abc',
6565
id: 'key-prod-backend-001',
6666
expiresAt: '2026-01-20T11:54:34.521671447-05:00',

packages/maas/frontend/src/app/pages/api-keys/CreateApiKeyModal.tsx

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ import {
44
Card,
55
CardBody,
66
CardTitle,
7-
ClipboardCopy,
8-
ClipboardCopyVariant,
7+
ClipboardCopyButton,
98
DescriptionList,
109
DescriptionListDescription,
1110
DescriptionListGroup,
@@ -17,6 +16,8 @@ import {
1716
FormHelperText,
1817
HelperText,
1918
HelperTextItem,
19+
InputGroup,
20+
InputGroupItem,
2021
MenuToggle,
2122
MenuToggleElement,
2223
Modal,
@@ -36,7 +37,7 @@ import {
3637
import TypeaheadSelect, {
3738
TypeaheadSelectOption,
3839
} from '@odh-dashboard/internal/components/TypeaheadSelect';
39-
import { CheckCircleIcon } from '@patternfly/react-icons';
40+
import { CheckCircleIcon, EyeIcon, EyeSlashIcon } from '@patternfly/react-icons';
4041
import React from 'react';
4142
import { z } from 'zod';
4243
import { useZodFormValidation } from '@odh-dashboard/internal/hooks/useZodFormValidation';
@@ -181,6 +182,10 @@ const CreateApiKeyModal: React.FC<CreateApiKeyModalProps> = ({ onClose }) => {
181182
const expirationLabel =
182183
formData.expirationOption === 'custom' ? `${formData.customDays} days` : selectedOption?.label;
183184

185+
const [isTokenVisible, setIsTokenVisible] = React.useState(false);
186+
const [isCopyTipCopied, setIsCopyTipCopied] = React.useState(false);
187+
const hiddenToken = '•'.repeat(Math.min(createdToken?.length ?? 0, 40));
188+
184189
return (
185190
<Modal variant={ModalVariant.medium} isOpen onClose={onClose}>
186191
<ModalHeader title={createdToken ? 'API key created' : 'Create API key'} />
@@ -213,17 +218,43 @@ const CreateApiKeyModal: React.FC<CreateApiKeyModalProps> = ({ onClose }) => {
213218
</Flex>
214219
</CardTitle>
215220
<CardBody>
216-
<ClipboardCopy
217-
variant={ClipboardCopyVariant.expansion}
218-
hoverTip="Copy"
219-
clickTip="Copied"
220-
data-testid="api-key-token-copy"
221-
onCopy={() => {
222-
navigator.clipboard.writeText(createdToken);
223-
}}
224-
>
225-
{createdToken}
226-
</ClipboardCopy>
221+
<InputGroup data-testid="api-key-token-copy-section">
222+
<InputGroupItem isFill>
223+
<TextInput
224+
readOnly
225+
aria-label="API key"
226+
value={isTokenVisible ? createdToken : hiddenToken}
227+
dir="ltr"
228+
/>
229+
</InputGroupItem>
230+
<InputGroupItem>
231+
<Button
232+
variant="control"
233+
data-testid="api-key-visibility-toggle"
234+
aria-label={isTokenVisible ? 'Hide API key' : 'Show API key'}
235+
icon={isTokenVisible ? <EyeSlashIcon /> : <EyeIcon />}
236+
onClick={() => setIsTokenVisible((v) => !v)}
237+
/>
238+
</InputGroupItem>
239+
<InputGroupItem>
240+
<ClipboardCopyButton
241+
id="api-key-created-copy"
242+
data-testid="api-key-token-copy-button"
243+
variant="control"
244+
aria-label="Copy API key"
245+
hasNoPadding
246+
onClick={() => {
247+
if (createdToken) {
248+
navigator.clipboard.writeText(createdToken);
249+
}
250+
setIsCopyTipCopied(true);
251+
}}
252+
onTooltipHidden={() => setIsCopyTipCopied(false)}
253+
>
254+
{isCopyTipCopied ? 'Copied' : 'Copy'}
255+
</ClipboardCopyButton>
256+
</InputGroupItem>
257+
</InputGroup>
227258
</CardBody>
228259
</Card>
229260
</StackItem>

0 commit comments

Comments
 (0)