Skip to content

Commit

Permalink
Merge pull request #204 from atlassian/ARC-2676-connected-state
Browse files Browse the repository at this point in the history
Arc-2676 connected state
  • Loading branch information
rachellerathbone authored Nov 23, 2023
2 parents 1613c78 + cf651a5 commit 11de4f2
Show file tree
Hide file tree
Showing 10 changed files with 369 additions and 33 deletions.
2 changes: 2 additions & 0 deletions app/jenkins-for-jira-ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from 'react-router';
import styled from '@emotion/styled';
import { view } from '@forge/bridge';
import { token } from '@atlaskit/tokens';
import { withLDProvider } from 'launchdarkly-react-client-sdk';
import { setGlobalTheme } from '@atlaskit/tokens';
import { InstallJenkins } from './components/ConnectJenkins/InstallJenkins/InstallJenkins';
Expand Down Expand Up @@ -54,6 +55,7 @@ export const environmentSettings = {
const AppContainer = styled.div`
color: #172B4D;
margin: 24px 36px 24px 0;
padding-bottom: ${token('space.300')};
`;

const App: React.FC = () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import moment from 'moment';
import '@testing-library/jest-dom/extend-expect';
import { ConnectedJenkinsServers, timeFromNow } from './ConnectedJenkinsServers';
import { EventType, JenkinsServer } from '../../../../src/common/types';

describe('timeFromNow util', () => {
test('returns correct string for hours', () => {
const date = moment().subtract(5, 'hours');
const result = timeFromNow(date);
expect(result).toEqual('About 5 hours ago');
});

test('returns correct string for single hour', () => {
const date = moment().subtract(1, 'hours');
const result = timeFromNow(date);
expect(result).toEqual('About 1 hour ago');
});

test('returns correct string for minutes', () => {
const date = moment().subtract(30, 'minutes');
const result = timeFromNow(date);
expect(result).toEqual('About 30 minutes ago');
});

test('returns correct string for single minute', () => {
const date = moment().subtract(1, 'minutes');
const result = timeFromNow(date);
expect(result).toEqual('About 1 minute ago');
});

test('returns correct string for seconds', () => {
const date = moment().subtract(45, 'seconds');
const result = timeFromNow(date);
expect(result).toEqual('About 45 seconds ago');
});

test('returns correct string for single second', () => {
const date = moment().subtract(1, 'seconds');
const result = timeFromNow(date);
expect(result).toEqual('About 1 second ago');
});
});

describe('ConnectedJenkinsServers suite', () => {
const mockConnectedJenkinsServer: JenkinsServer = {
name: 'mockServer',
uuid: '123',
pipelines: [
{
name: 'mockPipeline',
lastEventStatus: 'successful',
lastEventType: EventType.DEPLOYMENT,
lastEventDate: new Date()
}
]
};

test('renders column headers', () => {
render(<ConnectedJenkinsServers connectedJenkinsServer={mockConnectedJenkinsServer} />);
expect(screen.getByText('Pipeline')).toBeInTheDocument();
expect(screen.getByText('Event')).toBeInTheDocument();
expect(screen.getByText('Received')).toBeInTheDocument();
});

test('renders correct job & event content', () => {
render(<ConnectedJenkinsServers connectedJenkinsServer={mockConnectedJenkinsServer} />);
expect(screen.getByText(mockConnectedJenkinsServer.pipelines[0].name)).toBeInTheDocument();
expect(screen.getByText('successful deployment')).toBeInTheDocument();
});

test('renders correct time content', () => {
render(<ConnectedJenkinsServers connectedJenkinsServer={mockConnectedJenkinsServer} />);
const timeContent = moment().diff(moment(new Date(mockConnectedJenkinsServer.pipelines[0].lastEventDate)), 'hours') < 24
? timeFromNow(new Date(mockConnectedJenkinsServer.pipelines[0].lastEventDate))
: moment(new Date(mockConnectedJenkinsServer.pipelines[0].lastEventDate)).format('Do MMMM YYYY [at] hh:mma');
expect(screen.getByText(timeContent)).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import React from 'react';
import moment from 'moment/moment';
import { cx } from '@emotion/css';
import DynamicTable from '@atlaskit/dynamic-table';
import { JenkinsPipeline, JenkinsServer } from '../../../../src/common/types';
import {
mapLastEventStatus,
mapLastEventStatusIcons
} from '../JenkinsServerList/ConnectedServer/ConnectedServers';
import {
connectedStateCell,
connectedStateCellContainer,
connectedStateCellEvent,
connectedStateContainer
} from './ConnectionPanel.styles';

export const timeFromNow = (date: moment.MomentInput): string => {
const seconds = moment().diff(moment(date), 'seconds');
const minutes = moment().diff(moment(date), 'minutes');
const hours = moment().diff(moment(date), 'hours');

if (hours > 0) {
return `About ${hours} hour${hours > 1 ? 's' : ''} ago`;
}
if (minutes > 0) {
return `About ${minutes} minute${minutes > 1 ? 's' : ''} ago`;
}
return `About ${seconds} second${seconds > 1 ? 's' : ''} ago`;
};

type ConnectedStateProps = {
connectedJenkinsServer: JenkinsServer
};

type TableHead = {
cells: {
key: string;
content: string;
}[];
};

interface Row {
cells: {
key: string;
content: React.ReactNode;
}[];
}

const ConnectedJenkinsServers = ({ connectedJenkinsServer }: ConnectedStateProps): JSX.Element => {
const tableHead = ():TableHead => {
return {
cells: [
{
key: 'job',
content: 'Pipeline'
},
{
key: 'event',
content: 'Event'
},
{
key: 'time',
content: 'Received'
}
]
};
};

const rows = (serverName: string, serverId: string, pipelines: JenkinsPipeline[] = []): Row[] => {
return (
pipelines.map((pipeline: JenkinsPipeline) => ({
cells: [
{
key: 'job',
content: (
<div className={cx(connectedStateCellContainer)}>
<div className={cx(connectedStateCell)}>
{pipeline.name}
</div>
</div>
)
},
{
key: 'event',
content: (
<div className={cx(connectedStateCellContainer)}>
<>
<div className={cx(connectedStateCell)}>
{mapLastEventStatusIcons(pipeline.lastEventStatus)}
</div>
</>
<div className={cx(connectedStateCell, connectedStateCellEvent)}>
{mapLastEventStatus(pipeline.lastEventStatus)} {pipeline.lastEventType}
</div>
</div>
)
},
{
key: 'time',
content: (
<div className={cx(connectedStateCellContainer)}>
<div className={cx(connectedStateCell)}>
{
moment().diff(moment(new Date(pipeline.lastEventDate)), 'hours') < 24
? timeFromNow(new Date(pipeline.lastEventDate))
: moment(new Date(pipeline.lastEventDate)).format('Do MMMM YYYY [at] hh:mma')
}
</div>
</div>
)
}
]
}))
);
};

return (
<div className={cx(connectedStateContainer)}>
<DynamicTable
head={tableHead()}
rows={rows(connectedJenkinsServer.name, connectedJenkinsServer.uuid, connectedJenkinsServer.pipelines)}
loadingSpinnerSize='large'
/>
</div>
);
};

export { ConnectedJenkinsServers };
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,18 @@ export const connectionPanelMainContainer = css`
}
}
}
#connection-panel-tabs-0-tab {
padding: 0;
}
`;

export const connectionPanelMainTabs = css`
export const connectionPanelMainConnectedTabs = css`
margin-top: ${token('space.100')};
width: 100%;
`;

export const connectionPanelMainNotConnectedTabs = css`
align-items: center;
border-radius: 3px;
display: flex;
Expand All @@ -74,8 +83,9 @@ export const connectionPanelMainTabs = css`
width: 100%;
`;

// Not connected state
export const notConnectedStateContainer = css`
margin: 0 auto;
margin: ${token('space.0')} auto;
max-width: 420px;
text-align: center;
`;
Expand All @@ -102,7 +112,39 @@ export const notConnectedStateParagraph = css`
margin-bottom: ${token('space.400')};
div {
margin-bottom: ${token('space.300')}
margin-bottom: ${token('space.300')};
}
`;

// Connected state
export const connectedStateContainer = css`
margin-top: ${token('space.200')};
table {
border-bottom: none;
}
tr:hover {
background-color: #FFF;
}
`;

export const connectedStateCellContainer = css`
align-items: center;
display: flex;
`;

export const connectedStateCell = css`
font-size: 14px;
line-height: 20px;
margin: ${token('space.050')} 0;
`;

export const connectedStateCellEvent = css`
margin-left: ${token('space.100')};
&:first-letter {
text-transform: capitalize;
}
`;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ import { ConnectedState } from '../StatusLabel/StatusLabel';
describe('ConnectionPanelTop', () => {
test('renders with the correct content and styles for CONNECTED state', () => {
const ipAddress = '10.0.0.1';
render(<ConnectionPanelTop connectedState={ConnectedState.CONNECTED} ipAddress={ipAddress} />);
render(
<ConnectionPanelTop
connectedState={ConnectedState.CONNECTED}
ipAddress={ipAddress}
name="my server"
/>
);

const nameLabel = screen.getByText(/Insert name/i);
const ipAddressLabel = screen.getByText(`IP address: ${ipAddress}`);
Expand All @@ -20,7 +26,13 @@ describe('ConnectionPanelTop', () => {

test('renders with the correct content and styles for DUPLICATE state', () => {
const ipAddress = '10.0.0.1';
render(<ConnectionPanelTop connectedState={ConnectedState.DUPLICATE} ipAddress={ipAddress} />);
render(
<ConnectionPanelTop
connectedState={ConnectedState.DUPLICATE}
ipAddress={ipAddress}
name="my server"
/>
);

const nameLabel = screen.getByText(/Insert name/i);
const ipAddressLabel = screen.getByText(`IP address: ${ipAddress}`);
Expand All @@ -34,7 +46,13 @@ describe('ConnectionPanelTop', () => {

test('renders with the correct content and styles for PENDING state', () => {
const ipAddress = '10.0.0.1';
render(<ConnectionPanelTop connectedState={ConnectedState.PENDING} ipAddress={ipAddress} />);
render(
<ConnectionPanelTop
connectedState={ConnectedState.PENDING}
ipAddress={ipAddress}
name="my server"
/>
);

const nameLabel = screen.getByText(/Insert name/i);
const ipAddressLabel = screen.getByText(`IP address: ${ipAddress}`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,48 @@ import { ConnectionPanelMain } from './ConnectionPanelMain';
import { ConnectionPanelTop } from './ConnectionPanelTop';
import { ConnectedState } from '../StatusLabel/StatusLabel';
import { connectionPanelContainer } from './ConnectionPanel.styles';
import { JenkinsServer } from '../../../../src/common/types';
import { getAllJenkinsServers } from '../../api/getAllJenkinsServers';

// TODO - add DUPLICATE state once I'm pulling in new data from backend
export const addConnectedState = (servers: JenkinsServer[]): JenkinsServer[] => {
return servers.map((server: JenkinsServer) => ({
...server,
connectedState: server.pipelines.length === 0 ? ConnectedState.PENDING : ConnectedState.CONNECTED
}));
};

const ConnectionPanel = (): JSX.Element => {
// TODO - remove temp state and define pending/duplicate/connected state from data
const [connectedState, setConnectState] = useState<ConnectedState>(ConnectedState.PENDING);
const [jenkinsServers, setJenkinsServers] = useState<JenkinsServer[]>([]);

const fetchAllJenkinsServers = async () => {
const servers = await getAllJenkinsServers() || [];
const serversWithConnectedState = addConnectedState(servers);
setJenkinsServers(serversWithConnectedState);
};

useEffect(() => {
// TODO - update this based on data
setConnectState(ConnectedState.DUPLICATE);
fetchAllJenkinsServers();
}, []);

return (
<div className={cx(connectionPanelContainer)}>
<ConnectionPanelTop connectedState={connectedState} ipAddress="10.10.0.10"/>
<ConnectionPanelMain connectedState={connectedState} />
</div>
<>
{jenkinsServers.map(
(server: JenkinsServer, index: number): JSX.Element => (
<div className={cx(connectionPanelContainer)} key={index}>
<ConnectionPanelTop
name={server.name}
connectedState={server.connectedState || ConnectedState.PENDING}
ipAddress="10.10.0.10"
/>
<ConnectionPanelMain
connectedState={server.connectedState || ConnectedState.PENDING}
jenkinsServer={server}
/>
</div>
)
)}
</>
);
};

Expand Down
Loading

0 comments on commit 11de4f2

Please sign in to comment.