Skip to content

Commit

Permalink
ADM-998 [frontend][backend]: add none option when dora metric select …
Browse files Browse the repository at this point in the history
…only lead time for changes (#1582)

* ADM-998 [frontend][backend]: add none option when dora metric select only lead time for changes

* ADM-998 [backend]: fix sonar issues

* ADM-998 [frontend]: fix sonar issues

* ADM-998 [frontend]: modify e2e test when only select lead time for changes

* ADM-998 [frontend]: add e2e test for import none pipeline

* ADM-998 [docs]: update readme
  • Loading branch information
zhou-yinyuan authored Aug 20, 2024
1 parent 7fd271c commit ae0681a
Show file tree
Hide file tree
Showing 18 changed files with 369 additions and 88 deletions.
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ According to your selected required data, you need to input account settings for
| Pipeline change failure rate | Pipeline |
| Pipeline mean time to recovery | Pipeline |

If only `Lead time for changes` is selected among the four DORA metrics - `Lead time for changes`, `Deployment frequency`, `Pipeline change failure rate`, and `Pipeline mean time to recovery`, you will see an option for `None` in the pipeline tool configuration. If you choose the `None` option, when calculating `Lead time for changes`, only the `PR lead time` will be considered, and `pipeline lead time` will not be calculated.

![Image 3-4](https://cdn.jsdelivr.net/gh/au-heartbeat/data-hosting@main/readme/3-4-1.png)\
Image 3-4,Project config

Expand All @@ -198,10 +200,13 @@ _Image 3-5, create Jira token_

**The details for Pipeline:**

|Items|Description|
|---|---|
|PipelineTool| The pipeline tool you team use, currently heartbeat only support buildkite|
|Token|Generate buildkite token with below link, https://buildkite.com/user/api-access-tokens|
|Items| Description |
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|PipelineTool| The pipeline tool you team use, currently heartbeat only support buildkite. If only `Lead time for changes` is selected among the four DORA metrics, the `None` option will appear, indicating that when calculating `Lead time for changes`, only `PR lead time` will be considered, and `pipeline lead time` will not be calculated. |
|Token| Generate buildkite token with below link, https://buildkite.com/user/api-access-tokens |

![Image 3-5](https://cdn.jsdelivr.net/gh/au-heartbeat/data-hosting@main/readme/select-none-option-in-the-pipeline-configuration.png)

##### 3.1.3.2 Guideline for generating Buildkite token
Select organization for you pipeline
![Image 3-6](https://cdn.jsdelivr.net/gh/au-heartbeat/data-hosting@main/guideline-for-generating-token/generate-buildkite-token-org.png)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,12 @@ public class PipelineController {

@PostMapping("/{pipelineType}/verify")
public ResponseEntity<Void> verifyBuildKiteToken(
@Schema(type = "string", allowableValues = { "buildkite" },
@Schema(type = "string", allowableValues = { "buildkite", "none" },
accessMode = Schema.AccessMode.READ_ONLY) @PathVariable PipelineType pipelineType,
@Valid @RequestBody TokenParam tokenParam) {
buildKiteService.verifyToken(tokenParam.getToken());
if (!pipelineType.equals(PipelineType.NONE)) {
buildKiteService.verifyToken(tokenParam.getToken());
}
return ResponseEntity.noContent().build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
@Log4j2
public enum PipelineType {

BUILDKITE("buildkite");
BUILDKITE("buildkite"),

NONE("none");

public final String pipelineType;

Expand All @@ -16,6 +18,7 @@ public enum PipelineType {
public static PipelineType fromValue(String type) {
return switch (type) {
case "buildkite" -> BUILDKITE;
case "none" -> NONE;
default -> {
log.error("Failed to match Pipeline type: {} ", type);
throw new IllegalArgumentException("Pipeline type does not find!");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package heartbeat.controller.pipeline.dto.request;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
Expand All @@ -14,7 +14,7 @@
public class TokenParam {

@Valid
@NotBlank(message = "Token cannot be empty.")
@NotNull(message = "Token cannot be empty.")
private String token;

}
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ void shouldReturnNoContentIfNoStepsWhenCallBuildKite() throws Exception {
.andReturn()
.getResponse();

assertThat(response.getContentAsString()).isEqualTo("");
assertThat(response.getContentAsString()).isEmpty();
}

@Test
Expand All @@ -109,7 +109,23 @@ void shouldReturnNoContentWhenCorrectTokenCallBuildKite() throws Exception {
.andReturn()
.getResponse();

assertThat(response.getContentAsString()).isEqualTo("");
assertThat(response.getContentAsString()).isEmpty();
}

@Test
void shouldReturnNoContentWhenCorrectTokenCallNone() throws Exception {
ObjectMapper mapper = new ObjectMapper();
TokenParam tokenParam = TokenParam.builder().token(TEST_TOKEN).build();
doNothing().when(buildKiteService).verifyToken(any());

MockHttpServletResponse response = mockMvc
.perform(post("/pipelines/{pipelineType}/verify", "none").contentType(MediaType.APPLICATION_JSON)
.content(mapper.writeValueAsString(tokenParam)))
.andExpect(status().isNoContent())
.andReturn()
.getResponse();

assertThat(response.getContentAsString()).isEmpty();
}

@Test
Expand Down Expand Up @@ -153,7 +169,7 @@ void shouldReturnNoContentGivenPipelineInfoIsNullWhenCallingBuildKite() throws E
.andReturn()
.getResponse();

assertThat(response.getContentAsString()).isEqualTo("");
assertThat(response.getContentAsString()).isEmpty();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,21 @@
public class PipelineTypeTest {

@Test
public void shouldConvertValueToType() {
void shouldConvertValueToType() {
PipelineType buildKiteType = PipelineType.fromValue("buildkite");

assertEquals(buildKiteType, PipelineType.BUILDKITE);
assertEquals(PipelineType.BUILDKITE, buildKiteType);
}

@Test
public void shouldThrowExceptionWhenDateTypeNotSupported() {
void shouldConvertValueToTypeWhenTypeIsNone() {
PipelineType buildKiteType = PipelineType.fromValue("none");

assertEquals(PipelineType.NONE, buildKiteType);
}

@Test
void shouldThrowExceptionWhenDateTypeNotSupported() {
assertThatThrownBy(() -> PipelineType.fromValue("unknown")).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Pipeline type does not find!");
}
Expand Down
50 changes: 48 additions & 2 deletions frontend/__tests__/containers/ConfigStep/ConfigStep.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
CHINA_CALENDAR,
VIETNAM_CALENDAR,
TIMEOUT_ALERT,
LEAD_TIME_FOR_CHANGES,
PIPELINE_TOOL_TYPES,
} from '../../fixtures';
import {
basicInfoSchema,
Expand All @@ -38,10 +40,11 @@ import {
} from '@src/containers/ConfigStep/Form/useDefaultValues';
import { fillSourceControlFieldsInformation } from '@test/containers/ConfigStep/SourceControl.test';
import { fillPipelineToolFieldsInformation } from '@test/containers/ConfigStep/PipelineTool.test';
import { AxiosRequestErrorCode, PIPELINE_TOOL_NONE_OPTION } from '@src/constants/resources';
import { fillBoardFieldsInformation } from '@test/containers/ConfigStep/Board.test';
import { act, render, screen, waitFor, within } from '@testing-library/react';
import { AxiosRequestErrorCode } from '@src/constants/resources';
import { boardClient } from '@src/clients/board/BoardClient';
import { saveVersion } from '@src/context/meta/metaSlice';
import { setupStore } from '../../utils/setupStoreUtil';
import { TimeoutError } from '@src/errors/TimeoutError';
import { yupResolver } from '@hookform/resolvers/yup';
Expand Down Expand Up @@ -114,8 +117,9 @@ const ConfigStepWithFormInstances = () => {
};

describe('ConfigStep', () => {
const setup = () => {
const setup = (version: string = '1.2.1') => {
store = setupStore();
store.dispatch(saveVersion(version));
return render(
<Provider store={store}>
<ConfigStepWithFormInstances />
Expand Down Expand Up @@ -532,4 +536,46 @@ describe('ConfigStep', () => {

expect(screen.queryByLabelText(TIMEOUT_ALERT)).not.toBeInTheDocument();
});

it('should switch to buildKite when add a new dora metrics after pipeline tool is none', async () => {
setup();
await userEvent.click(screen.getByRole('combobox', { name: REQUIRED_DATA }));
let requireDateSelection = within(screen.getByRole('listbox'));
await userEvent.click(requireDateSelection.getByRole('option', { name: LEAD_TIME_FOR_CHANGES }));
await closeMuiModal(userEvent);

await userEvent.click(screen.getByRole('combobox', { name: 'Pipeline Tool' }));
let listBox = within(screen.getByRole('listbox'));
let options = listBox.getAllByRole('option');

expect(options.length).toEqual(2);

const buildKiteOption = options[0];
const noneOption = options[1];

expect(buildKiteOption.getAttribute('data-value')).toEqual(PIPELINE_TOOL_TYPES.BUILD_KITE);
expect(noneOption.getAttribute('data-value')).toEqual(PIPELINE_TOOL_NONE_OPTION);

await userEvent.click(noneOption);
await userEvent.click(screen.getByRole('combobox', { name: REQUIRED_DATA }));
requireDateSelection = within(screen.getByRole('listbox'));
await userEvent.click(requireDateSelection.getByRole('option', { name: DEPLOYMENT_FREQUENCY }));
await closeMuiModal(userEvent);
await userEvent.click(screen.getByRole('combobox', { name: 'Pipeline Tool' }));
listBox = within(screen.getByRole('listbox'));
options = listBox.getAllByRole('option');

expect(options.length).toEqual(1);
expect(options[0].getAttribute('data-value')).toEqual(PIPELINE_TOOL_TYPES.BUILD_KITE);

const tokenInput = within(screen.getByTestId('pipelineToolTextField')).getByLabelText(
PIPELINE_TOOL_TOKEN_INPUT_LABEL,
) as HTMLInputElement;

await waitFor(() => {
expect(within(screen.getByLabelText('Pipeline Tool Config')).getByText('Verify')).toBeDisabled();
expect(tokenInput).toBeInTheDocument();
expect(tokenInput.value).toEqual('');
});
});
});
31 changes: 29 additions & 2 deletions frontend/__tests__/containers/ConfigStep/PipelineTool.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ import {
TIMEOUT_ALERT_ARIA_LABEL,
} from '../../fixtures';
import { pipelineToolDefaultValues } from '@src/containers/ConfigStep/Form/useDefaultValues';
import { AxiosRequestErrorCode, PIPELINE_TOOL_NONE_OPTION } from '@src/constants/resources';
import { pipelineToolClient } from '@src/clients/pipeline/PipelineToolClient';
import { pipelineToolSchema } from '@src/containers/ConfigStep/Form/schema';
import { render, screen, waitFor, within } from '@testing-library/react';
import { PipelineTool } from '@src/containers/ConfigStep/PipelineTool';
import { AxiosRequestErrorCode } from '@src/constants/resources';
import { updateMetrics } from '@src/context/config/configSlice';
import { saveVersion } from '@src/context/meta/metaSlice';
import { setupStore } from '../../utils/setupStoreUtil';
import { FormProvider } from '@test/utils/FormProvider';
import { TimeoutError } from '@src/errors/TimeoutError';
Expand Down Expand Up @@ -60,8 +62,10 @@ describe('PipelineTool', () => {
const onReset = jest.fn();
const onSetResetFields = jest.fn();
store = setupStore();
const setup = () => {
const setup = (doraMetrics: string[] = ['Lead time for changes', 'Deployment frequency']) => {
store = setupStore();
store.dispatch(updateMetrics(doraMetrics));
store.dispatch(saveVersion('1.2.1'));
return render(
<Provider store={store}>
<FormProvider schema={pipelineToolSchema} defaultValues={pipelineToolDefaultValues}>
Expand Down Expand Up @@ -275,4 +279,27 @@ describe('PipelineTool', () => {

expect(verifyButton).toBeEnabled();
});

it('should not show token when select none', async () => {
setup(['Lead time for changes']);

await userEvent.click(screen.getByRole('combobox', { name: 'Pipeline Tool' }));
const listBox = within(screen.getByRole('listbox'));
const options = listBox.getAllByRole('option');

expect(options.length).toEqual(2);

const buildKiteOption = options[0];
const noneOption = options[1];

expect(buildKiteOption.getAttribute('data-value')).toEqual(PIPELINE_TOOL_TYPES.BUILD_KITE);
expect(noneOption.getAttribute('data-value')).toEqual(PIPELINE_TOOL_NONE_OPTION);

await userEvent.click(noneOption);

expect(screen.queryByRole('button', { name: /verify/i })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: /reset/i })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: /verified/i })).not.toBeInTheDocument();
expect(screen.queryByRole('Token *') as HTMLInputElement).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"projectName": "demo show",
"dateRange": [
{
"startDate": "2024-06-01T00:00:00.000+08:00",
"endDate": "2024-06-14T23:59:59.999+08:00"
},
{
"startDate": "2024-05-15T00:00:00.000+08:00",
"endDate": "2024-05-28T23:59:59.999+08:00"
}
],
"calendarType": "CN",
"metrics": ["Velocity", "Cycle time", "Classification", "Rework times", "Lead time for changes"],
"sortType": "DESCENDING",
"board": {
"type": "Jira",
"boardId": "2",
"email": "[email protected]",
"projectKey": "ADM",
"site": "dorametrics",
"token": "<E2E_TOKEN_JIRA>"
},
"pipelineTool": {
"type": "None",
"token": ""
},
"sourceControl": {
"type": "GitHub",
"token": "<E2E_TOKEN_GITHUB>"
}
}
39 changes: 34 additions & 5 deletions frontend/e2e/pages/metrics/config-step.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export class ConfigStep {
readonly pipelineToolContainer: Locator;
readonly pipelineToolTypeSelect: Locator;
readonly pipelineToolTypeBuildKiteOption: Locator;
readonly pipelineToolTypeNoneOption: Locator;
readonly pipelineToolTokenInput: Locator;
readonly pipelineToolVerifyButton: Locator;
readonly pipelineToolVerifiedButton: Locator;
Expand Down Expand Up @@ -174,6 +175,7 @@ export class ConfigStep {
this.pipelineToolContainer = page.getByLabel('Pipeline Tool Config');
this.pipelineToolTypeSelect = this.pipelineToolContainer.getByLabel('Pipeline Tool *');
this.pipelineToolTypeBuildKiteOption = page.getByRole('option', { name: 'BuildKite' });
this.pipelineToolTypeNoneOption = page.getByRole('option', { name: 'None' });
this.pipelineToolTokenInput = this.pipelineToolContainer.getByLabel('Token');
this.pipelineToolVerifyButton = this.pipelineToolContainer.getByRole('button', { name: 'Verify' });
this.pipelineToolVerifiedButton = this.pipelineToolContainer.getByRole('button', { name: 'Verified' });
Expand Down Expand Up @@ -357,7 +359,7 @@ export class ConfigStep {
}

async selectDeploymentFrequencyMetrics() {
await this.requiredMetricsLabel.click();
await this.requiredMetricsLabel.first().click();
await this.requiredMetricsDeploymentFrequencyOption.click();
await this.page.keyboard.press('Escape');
}
Expand Down Expand Up @@ -392,10 +394,15 @@ export class ConfigStep {
await expect(this.boardTokenInput).toBeHidden();
}

async checkPipelineToolFormVisible() {
async checkPipelineToolFormVisible(selectName: string = 'BuildKite') {
await expect(this.pipelineToolContainer).toBeVisible();
await expect(this.pipelineToolTypeSelect).toBeVisible();
await expect(this.pipelineToolTokenInput).toBeVisible();
await expect(await this.pipelineToolTypeSelect.textContent()).toContain(selectName);
if (selectName !== 'None') {
await expect(this.pipelineToolTokenInput).toBeVisible();
} else {
await expect(this.pipelineToolTokenInput).not.toBeVisible();
}
}

async checkPipelineToolFormInvisible() {
Expand Down Expand Up @@ -483,9 +490,23 @@ export class ConfigStep {
}

async fillPipelineToolForm({ token }: IPipelineToolData) {
await this.pipelineToolTokenInput.fill(token);
await this.pipelineToolTypeSelect.click();
await this.pipelineToolTypeBuildKiteOption.click();
await this.pipelineToolTokenInput.fill(token);
}

async clickNoneOptionInPipelineToolForm() {
await this.pipelineToolTypeSelect.click();
await expect(this.pipelineToolTypeNoneOption).toBeVisible();
await this.pipelineToolTypeNoneOption.click();
}

async verifiedButtonInPipelineToolForm() {
await expect(this.pipelineToolVerifiedButton).toBeVisible();
}

async verifiedButtonNotInPipelineToolForm() {
await expect(this.pipelineToolVerifiedButton).not.toBeVisible();
}

async fillAndVerifyPipelineToolForm(pipelineToolData: IPipelineToolData) {
Expand All @@ -495,7 +516,15 @@ export class ConfigStep {

await this.pipelineToolVerifyButton.click();

await expect(this.pipelineToolVerifiedButton).toBeVisible();
await this.verifiedButtonInPipelineToolForm();
}

async verifyButtonNotClickableInPipelineToolForm() {
await expect(this.pipelineToolVerifyButton).toBeDisabled();
}

async verifyButtonNotExistInPipelineToolForm() {
await expect(this.pipelineToolVerifyButton).not.toBeVisible();
}

async fillSourceControlForm({ token }: ISourceControlData) {
Expand Down
Loading

0 comments on commit ae0681a

Please sign in to comment.