Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ARC-2723 add share modal #219

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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/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
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