diff --git a/.changeset/thick-pets-attend.md b/.changeset/thick-pets-attend.md new file mode 100644 index 0000000000..4a58e4807c --- /dev/null +++ b/.changeset/thick-pets-attend.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Don't show login prompt if token is set in the state diff --git a/plugins/scaffolder-react/api-report.md b/plugins/scaffolder-react/api-report.md index 400cf24c53..311f4116b3 100644 --- a/plugins/scaffolder-react/api-report.md +++ b/plugins/scaffolder-react/api-report.md @@ -455,7 +455,9 @@ export interface ScaffolderUseTemplateSecrets { // @public export const SecretsContextProvider: ( - props: PropsWithChildren<{}>, + props: PropsWithChildren<{ + initialSecrets?: Record; + }>, ) => React_2.JSX.Element; // @public diff --git a/plugins/scaffolder-react/src/secrets/SecretsContext.test.tsx b/plugins/scaffolder-react/src/secrets/SecretsContext.test.tsx index 4d56b5764c..5252cf77bd 100644 --- a/plugins/scaffolder-react/src/secrets/SecretsContext.test.tsx +++ b/plugins/scaffolder-react/src/secrets/SecretsContext.test.tsx @@ -35,4 +35,20 @@ describe('SecretsContext', () => { expect(result.current.hook?.secrets.foo).toEqual('bar'); }); + + it('should create SecretsContextProvider with initial secrets', async () => { + const { result } = renderHook( + () => ({ + hook: useTemplateSecrets(), + }), + { + wrapper: ({ children }: React.PropsWithChildren<{}>) => ( + + {children} + + ), + }, + ); + expect(result.current.hook?.secrets.foo).toEqual('bar'); + }); }); diff --git a/plugins/scaffolder-react/src/secrets/SecretsContext.tsx b/plugins/scaffolder-react/src/secrets/SecretsContext.tsx index 3cbd23597a..64b25d0e8f 100644 --- a/plugins/scaffolder-react/src/secrets/SecretsContext.tsx +++ b/plugins/scaffolder-react/src/secrets/SecretsContext.tsx @@ -43,8 +43,13 @@ const SecretsContext = createVersionedContext<{ * The Context Provider that holds the state for the secrets. * @public */ -export const SecretsContextProvider = (props: PropsWithChildren<{}>) => { - const [secrets, setSecrets] = useState>({}); +export const SecretsContextProvider = ( + props: PropsWithChildren<{ initialSecrets?: Record }>, +) => { + const { initialSecrets = {} } = props; + const [secrets, setSecrets] = useState>({ + ...initialSecrets, + }); return ( { byHost: () => ({ type: 'github' }), }; - const mockScmAuthApi: Partial = { - getCredentials: jest.fn().mockResolvedValue({ token: 'abc123' }), - }; + let mockScmAuthApi: Partial; + + beforeEach(() => { + mockScmAuthApi = { + getCredentials: jest.fn().mockResolvedValue({ token: 'abc123' }), + }; + }); describe('happy path rendering', () => { it('should render the repo url picker with minimal props', async () => { @@ -350,5 +354,64 @@ describe('RepoUrlPicker', () => { secrets: { testKey: 'abc123' }, }); }); + + it('should not call the scmAuthApi if secret is available in the state', async () => { + const SecretsComponent = () => { + const { secrets } = useTemplateSecrets(); + return ( +
{JSON.stringify({ secrets })}
+ ); + }; + const { getAllByRole, getByTestId } = await renderInTestApp( + + +
, + }} + /> + + + , + ); + + const [ownerInput, repoInput] = getAllByRole('textbox'); + + await act(async () => { + fireEvent.change(ownerInput, { target: { value: 'backstage' } }); + fireEvent.change(repoInput, { target: { value: 'repo123' } }); + + // need to wait for the debounce to finish + await new Promise(resolve => setTimeout(resolve, 600)); + }); + + // as we already have a secret in the state, getCredentials should not be called again. + expect(mockScmAuthApi.getCredentials).toHaveBeenCalledTimes(0); + + const currentSecrets = JSON.parse( + getByTestId('current-secrets').textContent!, + ); + + expect(currentSecrets).toEqual({ + secrets: { testKey: 'abc123' }, + }); + }); }); }); diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx index b5d5e8d572..abc12093ab 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx @@ -51,7 +51,7 @@ export const RepoUrlPicker = (props: RepoUrlPickerProps) => { ); const integrationApi = useApi(scmIntegrationsApiRef); const scmAuthApi = useApi(scmAuthApiRef); - const { setSecrets } = useTemplateSecrets(); + const { secrets, setSecrets } = useTemplateSecrets(); const allowedHosts = useMemo( () => uiSchema?.['ui:options']?.allowedHosts ?? [], [uiSchema], @@ -132,6 +132,11 @@ export const RepoUrlPicker = (props: RepoUrlPickerProps) => { return; } + // don't show login prompt if secret value is already in state + if (secrets[requestUserCredentials.secretsKey]) { + return; + } + // previously, we were encodeURI for state.host, workspace and state.repoName separately. // That created an issue where GitLab workspace can be nested like groupA/subgroupB // when we encodeURi separately and then join, the URL will be malformed and