Skip to content

Commit

Permalink
add share modal
Browse files Browse the repository at this point in the history
  • Loading branch information
rachellerathbone committed Dec 11, 2023
1 parent d90a139 commit c703afe
Show file tree
Hide file tree
Showing 12 changed files with 270 additions and 46 deletions.
1 change: 1 addition & 0 deletions app/jenkins-for-jira-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@atlaskit/progress-tracker": "8.5.2",
"@atlaskit/spinner": "15.5.3",
"@atlaskit/tabs": "^13.4.9",
"@atlaskit/textarea": "^5.0.0",
"@atlaskit/textfield": "5.6.4",
"@atlaskit/theme": "12.5.5",
"@atlaskit/tokens": "^1.28.1",
Expand Down
22 changes: 22 additions & 0 deletions app/jenkins-for-jira-ui/src/api/fetchGlobalPageUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { invoke } from '@forge/bridge';

type FetchGlobalPageUrlContext = {
siteUrl: string;
appId: string;
environmentId: string;
};

const fetchGlobalPageUrl = async (): Promise<string> => {
const context: FetchGlobalPageUrlContext = await invoke('fetchAppData');
const {
siteUrl,
appId,
environmentId
} = context;

return `${siteUrl}/jira/settings/apps/${appId}/${environmentId}`;
};

export {
fetchGlobalPageUrl
};
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe('redirectFromGetStarted', () => {

(invoke as jest.Mock).mockResolvedValue(contextData);
await redirectFromGetStarted();
expect(invoke).toHaveBeenCalledWith('redirectFromGetStarted');
expect(invoke).toHaveBeenCalledWith('fetchAppData');
expect(router.navigate).toHaveBeenCalledWith(
`${contextData.siteUrl}/jira/settings/apps/${contextData.appId}/${contextData.environmentId}/`
);
Expand All @@ -33,7 +33,7 @@ describe('redirectFromGetStarted', () => {

(invoke as jest.Mock).mockResolvedValue(contextData);
await redirectFromGetStarted();
expect(invoke).toHaveBeenCalledWith('redirectFromGetStarted');
expect(invoke).toHaveBeenCalledWith('fetchAppData');
expect(router.navigate).not.toHaveBeenCalledWith(
`${contextData.siteUrl}/jira/settings/apps/${contextData.appId}/${contextData.environmentId}/`
);
Expand Down
2 changes: 1 addition & 1 deletion app/jenkins-for-jira-ui/src/api/redirectFromGetStarted.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ interface Context {
}

const redirectFromGetStarted = async (): Promise<string> => {
const context: Context = await invoke('redirectFromGetStarted');
const context: Context = await invoke('fetchAppData');
const {
siteUrl,
appId,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
import Button, { LoadingButton } from '@atlaskit/button';
import Button, { LoadingButton, Appearance } from '@atlaskit/button';
import React from 'react';
import Modal, {
Appearance,
KeyboardOrMouseEvent,
ModalBody,
ModalFooter,
ModalHeader,
ModalTitle,
ModalTransition
ModalTransition,
Appearance as ModalAppearance
} from '@atlaskit/modal-dialog';
import { Appearance as ButtonAppearance } from '@atlaskit/button';
import EditorSuccessIcon from '@atlaskit/icon/glyph/editor/success';
import { cx } from '@emotion/css';
import { JenkinsServer } from '../../../../../src/common/types';
import { loadingIcon } from './JenkinsModal.styles';
import {
copyToClipboard,
copyToClipboardContainer,
secondaryButtonContainer
} from '../../ServerManagement/ServerManagement.styles';

type ModalProps = {
server?: JenkinsServer;
show: boolean;
modalAppearance: Appearance;
modalAppearance?: ModalAppearance;
title: string;
body: (string | React.ReactElement<any>)[];
onClose(e: KeyboardOrMouseEvent): void;
primaryButtonAppearance: ButtonAppearance;
primaryButtonAppearance: Appearance;
primaryButtonLabel: string;
secondaryButtonAppearance: Appearance;
secondaryButtonLabel: string | React.ReactElement<any>;
Expand All @@ -29,6 +35,7 @@ type ModalProps = {
): Promise<void> | void;
dataTestId: string;
isLoading?: boolean;
isCopiedToClipboard?: boolean;
};

const JenkinsModal: React.FC<ModalProps> = ({
Expand All @@ -44,35 +51,61 @@ const JenkinsModal: React.FC<ModalProps> = ({
secondaryButtonAppearance,
secondaryButtonLabel,
secondaryButtonOnClick,
isLoading
isLoading,
isCopiedToClipboard
}: ModalProps): JSX.Element => {
return (
<ModalTransition>
{show && (
<Modal onClose={onClose} testId={dataTestId}>
<ModalHeader>
<ModalTitle appearance={modalAppearance}>{title}</ModalTitle>
{
modalAppearance
? (
<ModalTitle appearance={modalAppearance}>{title}</ModalTitle>
)
: (
<ModalTitle>{title}</ModalTitle>
)
}
</ModalHeader>
<ModalBody>{body}</ModalBody>
<ModalFooter>
<Button appearance={primaryButtonAppearance} onClick={onClose} testId='closeButton'>
{primaryButtonLabel}
</Button>

{isLoading
? <LoadingButton appearance='warning' isLoading className={loadingIcon} />
: <Button
appearance={secondaryButtonAppearance}
onClick={(event: JenkinsServer | KeyboardOrMouseEvent) =>
dataTestId === 'disconnectModal'
? secondaryButtonOnClick(server)
: secondaryButtonOnClick(event)
}
testId='secondaryButton'
>
{secondaryButtonLabel}
</Button>
}
{
isLoading
? (
<LoadingButton appearance='warning' isLoading className={loadingIcon} />
) : (
<div className={cx(secondaryButtonContainer)}>
<Button
appearance={secondaryButtonAppearance}
onClick={(event: JenkinsServer | KeyboardOrMouseEvent) =>
(dataTestId === 'disconnectModal'
? secondaryButtonOnClick(server)
: secondaryButtonOnClick(event))
}
testId='secondaryButton'
>
{secondaryButtonLabel}
</Button>

{isCopiedToClipboard && (
<div className={cx(copyToClipboardContainer)}>
<EditorSuccessIcon
primaryColor="#23a06b"
label="Copied to clipboard successfully"
/>
<div className={cx(copyToClipboard)}>
Copied to clipboard
</div>
</div>
)}
</div>
)}

</ModalFooter>
</Modal>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,43 @@
import { css } from '@emotion/css';
import { token } from '@atlaskit/tokens';

export const mainPageContainer = css`
export const serverManagementContainer = css`
margin: 0 auto;
max-width: 936px;
`;

export const modalHeaderContainer = css`
margin-top: 40px;
`;

export const shareModalInstruction = css`
margin: ${token('space.200')} auto;
`;

export const secondaryButtonContainer = css`
position: relative;
`;

export const copyToClipboardContainer = css`
align-items: center;
background-color: #fff;
border-radius: 3px;
box-shadow: 0px 2px 4px 0px #091E4240;
display: flex;
max-width: 160px;
position: absolute;
padding: ${token('space.200')};
top: -115%;
left: 68%;
transform: translate(-50%, -50%);
width: 194px;
z-index: 2;
`;

export const copyToClipboard = css`
margin-left: ${token('space.075')};
`;

// Top panel
export const topPanelContainer = css`
align-items: center;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react';
import {
render, screen, fireEvent, waitFor
} from '@testing-library/react';
import { getSiteNameFromUrl, ServerManagement } from './ServerManagement';

document.execCommand = jest.fn();

describe('ServerManagement Component', () => {
test('should copy to clipboard when "Copy to clipboard" is clicked', async () => {
render(<ServerManagement />);
fireEvent.click(screen.getByText('Share page'));
fireEvent.click(screen.getByText('Copy to clipboard'));

await waitFor(() => screen.getByText('Copied to clipboard'));
expect(document.execCommand).toHaveBeenCalledWith('copy');
});

test('should close the share modal when "Close" is clicked', async () => {
render(<ServerManagement />);
fireEvent.click(screen.getByText('Share page'));
expect(screen.getByText('Copy to clipboard')).toBeInTheDocument();

await waitFor(() => screen.getByText('Close'));

fireEvent.click(screen.getByText('Close'));

await waitFor(() => {
expect(screen.queryByText('Copy to clipboard')).not.toBeInTheDocument();
});
});

test('correctly extracts site name from URL', () => {
const url = 'https://testjira.atlassian.net/jira/apps/blah-blah';
const siteName = getSiteNameFromUrl(url);
expect(siteName).toEqual('testjira.atlassian.net');
});
});
Original file line number Diff line number Diff line change
@@ -1,34 +1,120 @@
import React from 'react';
import React, { useState, useRef, useEffect } from 'react';
import PageHeader from '@atlaskit/page-header';
import { ButtonGroup } from '@atlaskit/button';
import Button from '@atlaskit/button/standard-button';
import { headerContainer } from '../JenkinsServerList/JenkinsServerList.styles';
import { mainPageContainer } from './ServerManagement.styles';
import TextArea from '@atlaskit/textarea';
import { cx } from '@emotion/css';
import { serverManagementContainer, modalHeaderContainer, shareModalInstruction } from './ServerManagement.styles';
import { ConnectionPanel } from '../ConnectionPanel/ConnectionPanel';
import { TopPanel } from './TopPanel/TopPanel';
import { JenkinsModal } from '../JenkinsServerList/ConnectedServer/JenkinsModal';
import { fetchGlobalPageUrl } from '../../api/fetchGlobalPageUrl';

export const getSiteNameFromUrl = (url: string): string => {
try {
const urlObject = new URL(url);
return urlObject.hostname;
} catch (error) {
console.error('Error extracting site name:', error);
return '';
}
};

const ServerManagement = (): JSX.Element => {
const [showSharePage, setshowSharePage] = useState<boolean>(false);
const [isCopiedToClipboard, setIsCopiedToClipboard] = useState(false);
const [globalPageUrl, setGlobalPageUrl] = useState<string>('');
const textAreaRef = useRef<HTMLTextAreaElement>(null);

const handleShowSharePageModal = async () => {
setshowSharePage(true);
};

const handleCloseShowSharePageModal = async () => {
setshowSharePage(false);
};

const handleCopyToClipboard = async () => {
if (textAreaRef.current) {
textAreaRef.current.select();
document.execCommand('copy');
textAreaRef.current.setSelectionRange(textAreaRef.current.value.length, textAreaRef.current.value.length);

setIsCopiedToClipboard(true);

setTimeout(() => {
setIsCopiedToClipboard(false);
}, 2000);
}
};

useEffect(() => {
const fetchData = async () => {
try {
const url = await fetchGlobalPageUrl();
setGlobalPageUrl(url);
} catch (error) {
console.error('Error fetching data:', error);
}
};

fetchData();
}, []);

const pageHeaderActions = (
<ButtonGroup>
{/* TODO handle empty state - ARC-2730 connection wizard */}
<Button appearance="primary">
Connect a new Jenkins server
</Button>
{/* TODO - ARC-2723 share modal */}
<Button>Share page</Button>
<Button appearance="primary">Connect a new Jenkins server</Button>
<Button onClick={() => handleShowSharePageModal()}>Share page</Button>
</ButtonGroup>
);

// TODO handle empty state - ARC-2730 connection wizard
const sharePageMessage =
`Hi there,
Jenkins for Jira is now installed and connected on ${getSiteNameFromUrl(globalPageUrl)}.
To set up what build and deployment events Jenkins sends to Jira, follow the set up guide(s) on this page:
${globalPageUrl}
You'll need to follow the set up guide for each server connected.`;

return (
<div className={mainPageContainer}>
<div className={headerContainer}>
<div className={serverManagementContainer}>
<div className={modalHeaderContainer}>
<PageHeader actions={pageHeaderActions}>Jenkins for Jira</PageHeader>
</div>

<TopPanel />

<ConnectionPanel />

<JenkinsModal
dataTestId="share-page-modal"
show={showSharePage}
title="Share page"
body={[
<p key="share-message" className={cx(shareModalInstruction)}>
Share this link with your project teams to help them set up what
data they receive from Jenkins.
</p>,
<TextArea
key="text-area"
ref={textAreaRef}
value={sharePageMessage}
isReadOnly
minimumRows={5}
/>
]}
onClose={handleCloseShowSharePageModal}
primaryButtonAppearance="subtle"
primaryButtonLabel="Close"
secondaryButtonAppearance="primary"
secondaryButtonLabel="Copy to clipboard"
secondaryButtonOnClick={handleCopyToClipboard}
isCopiedToClipboard={isCopiedToClipboard}
/>
</div>
);
};
Expand Down
Loading

0 comments on commit c703afe

Please sign in to comment.