Skip to content

Commit

Permalink
[8.16] [Discover] Show the fetched Discover results even when histogr…
Browse files Browse the repository at this point in the history
…am request fails on some shards (#198553) (#199123)

# Backport

This will backport the following commits from `main` to `8.16`:
- [[Discover] Show the fetched Discover results even when histogram
request fails on some shards
(#198553)](#198553)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Julia
Rechkunova","email":"[email protected]"},"sourceCommit":{"committedDate":"2024-11-06T12:45:01Z","message":"[Discover]
Show the fetched Discover results even when histogram request fails on
some shards (#198553)\n\n- Closes
https://github.com/elastic/kibana/issues/198496\r\n\r\n##
Summary\r\n\r\nThis PR fixes an issue when the histogram request returns
only a partial\r\nresult (0 or greater than 0) by adding a warning icon
next to the total\r\nhits counter and not blocking the whole page with
\"No results\" message\r\n(when partial result with 0 hits from
histogram).\r\n\r\n<img width=\"1436\" alt=\"Screenshot 2024-10-31 at 15
45
17\"\r\nsrc=\"https://github.com/user-attachments/assets/9a769fe6-bdcf-4d20-ae6e-698a5b08d76f\">\r\n\r\n###
Testing\r\n\r\nExecute the following and open `example*` data view in
Discover.\r\n\r\n```\r\nPUT example1\r\nPUT example1/_mapping\r\n{\r\n
\"properties\": {\r\n \"message\": {\r\n \"type\": \"text\"\r\n },\r\n
\"date\": {\r\n \"type\": \"date\"\r\n }\r\n }\r\n}\r\nPUT
example1/_doc/11\r\n{\r\n \"message\": \"11\",\r\n \"date\":
\"2024-11-11T12:10:30Z\"\r\n}\r\nPUT example1/_doc/12\r\n{\r\n
\"message\": \"22\",\r\n \"date\":
\"2024-11-12T12:10:30Z\"\r\n}\r\n\r\nPUT example2\r\nPUT
example2/_mapping\r\n{\r\n \"properties\": {\r\n \"message\": {\r\n
\"type\": \"keyword\"\r\n },\r\n \"date\": {\r\n \"type\": \"date\"\r\n
}\r\n }\r\n}\r\nPUT example2/_doc/21\r\n{\r\n \"message\": \"21\",\r\n
\"date\": \"2024-12-01T12:10:30Z\"\r\n}\r\nPUT example2/_doc/22\r\n{\r\n
\"message\": \"22\",\r\n \"date\":
\"2024-12-02T12:10:30Z\"\r\n}\r\n```\r\n\r\nThen add `message` as a
breakdown field.\r\n\r\nNotice that the histogram gets some partial
results:\r\n<img width=\"1563\" alt=\"Screenshot 2024-10-31 at 16 11
14\"\r\nsrc=\"https://github.com/user-attachments/assets/8a53f661-38a2-48f8-b082-823de77ac4f2\">\r\n\r\nNow,
add a filter for `_id: 11` and notice that the histogram request\r\nhas
no results (it partially failed on some shards) but Discover
still\r\nrenders the table:\r\n<img width=\"1564\" alt=\"Screenshot
2024-10-31 at 16 11
31\"\r\nsrc=\"https://github.com/user-attachments/assets/e154ab5d-c5d4-4703-abd4-7bf3cd7a15fb\">\r\n\r\n\r\n###
Checklist\r\n\r\n- [x] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: Davis McPhee
<[email protected]>","sha":"c1e00a887173e8b86fd58fa0e7e768e4f0b1643c","branchLabelMapping":{"^v9.0.0$":"main","^v8.17.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:fix","v9.0.0","Team:DataDiscovery","v8.16.0","backport:version","v8.17.0"],"title":"[Discover]
Show the fetched Discover results even when histogram request fails on
some
shards","number":198553,"url":"https://github.com/elastic/kibana/pull/198553","mergeCommit":{"message":"[Discover]
Show the fetched Discover results even when histogram request fails on
some shards (#198553)\n\n- Closes
https://github.com/elastic/kibana/issues/198496\r\n\r\n##
Summary\r\n\r\nThis PR fixes an issue when the histogram request returns
only a partial\r\nresult (0 or greater than 0) by adding a warning icon
next to the total\r\nhits counter and not blocking the whole page with
\"No results\" message\r\n(when partial result with 0 hits from
histogram).\r\n\r\n<img width=\"1436\" alt=\"Screenshot 2024-10-31 at 15
45
17\"\r\nsrc=\"https://github.com/user-attachments/assets/9a769fe6-bdcf-4d20-ae6e-698a5b08d76f\">\r\n\r\n###
Testing\r\n\r\nExecute the following and open `example*` data view in
Discover.\r\n\r\n```\r\nPUT example1\r\nPUT example1/_mapping\r\n{\r\n
\"properties\": {\r\n \"message\": {\r\n \"type\": \"text\"\r\n },\r\n
\"date\": {\r\n \"type\": \"date\"\r\n }\r\n }\r\n}\r\nPUT
example1/_doc/11\r\n{\r\n \"message\": \"11\",\r\n \"date\":
\"2024-11-11T12:10:30Z\"\r\n}\r\nPUT example1/_doc/12\r\n{\r\n
\"message\": \"22\",\r\n \"date\":
\"2024-11-12T12:10:30Z\"\r\n}\r\n\r\nPUT example2\r\nPUT
example2/_mapping\r\n{\r\n \"properties\": {\r\n \"message\": {\r\n
\"type\": \"keyword\"\r\n },\r\n \"date\": {\r\n \"type\": \"date\"\r\n
}\r\n }\r\n}\r\nPUT example2/_doc/21\r\n{\r\n \"message\": \"21\",\r\n
\"date\": \"2024-12-01T12:10:30Z\"\r\n}\r\nPUT example2/_doc/22\r\n{\r\n
\"message\": \"22\",\r\n \"date\":
\"2024-12-02T12:10:30Z\"\r\n}\r\n```\r\n\r\nThen add `message` as a
breakdown field.\r\n\r\nNotice that the histogram gets some partial
results:\r\n<img width=\"1563\" alt=\"Screenshot 2024-10-31 at 16 11
14\"\r\nsrc=\"https://github.com/user-attachments/assets/8a53f661-38a2-48f8-b082-823de77ac4f2\">\r\n\r\nNow,
add a filter for `_id: 11` and notice that the histogram request\r\nhas
no results (it partially failed on some shards) but Discover
still\r\nrenders the table:\r\n<img width=\"1564\" alt=\"Screenshot
2024-10-31 at 16 11
31\"\r\nsrc=\"https://github.com/user-attachments/assets/e154ab5d-c5d4-4703-abd4-7bf3cd7a15fb\">\r\n\r\n\r\n###
Checklist\r\n\r\n- [x] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: Davis McPhee
<[email protected]>","sha":"c1e00a887173e8b86fd58fa0e7e768e4f0b1643c"}},"sourceBranch":"main","suggestedTargetBranches":["8.16","8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/198553","number":198553,"mergeCommit":{"message":"[Discover]
Show the fetched Discover results even when histogram request fails on
some shards (#198553)\n\n- Closes
https://github.com/elastic/kibana/issues/198496\r\n\r\n##
Summary\r\n\r\nThis PR fixes an issue when the histogram request returns
only a partial\r\nresult (0 or greater than 0) by adding a warning icon
next to the total\r\nhits counter and not blocking the whole page with
\"No results\" message\r\n(when partial result with 0 hits from
histogram).\r\n\r\n<img width=\"1436\" alt=\"Screenshot 2024-10-31 at 15
45
17\"\r\nsrc=\"https://github.com/user-attachments/assets/9a769fe6-bdcf-4d20-ae6e-698a5b08d76f\">\r\n\r\n###
Testing\r\n\r\nExecute the following and open `example*` data view in
Discover.\r\n\r\n```\r\nPUT example1\r\nPUT example1/_mapping\r\n{\r\n
\"properties\": {\r\n \"message\": {\r\n \"type\": \"text\"\r\n },\r\n
\"date\": {\r\n \"type\": \"date\"\r\n }\r\n }\r\n}\r\nPUT
example1/_doc/11\r\n{\r\n \"message\": \"11\",\r\n \"date\":
\"2024-11-11T12:10:30Z\"\r\n}\r\nPUT example1/_doc/12\r\n{\r\n
\"message\": \"22\",\r\n \"date\":
\"2024-11-12T12:10:30Z\"\r\n}\r\n\r\nPUT example2\r\nPUT
example2/_mapping\r\n{\r\n \"properties\": {\r\n \"message\": {\r\n
\"type\": \"keyword\"\r\n },\r\n \"date\": {\r\n \"type\": \"date\"\r\n
}\r\n }\r\n}\r\nPUT example2/_doc/21\r\n{\r\n \"message\": \"21\",\r\n
\"date\": \"2024-12-01T12:10:30Z\"\r\n}\r\nPUT example2/_doc/22\r\n{\r\n
\"message\": \"22\",\r\n \"date\":
\"2024-12-02T12:10:30Z\"\r\n}\r\n```\r\n\r\nThen add `message` as a
breakdown field.\r\n\r\nNotice that the histogram gets some partial
results:\r\n<img width=\"1563\" alt=\"Screenshot 2024-10-31 at 16 11
14\"\r\nsrc=\"https://github.com/user-attachments/assets/8a53f661-38a2-48f8-b082-823de77ac4f2\">\r\n\r\nNow,
add a filter for `_id: 11` and notice that the histogram request\r\nhas
no results (it partially failed on some shards) but Discover
still\r\nrenders the table:\r\n<img width=\"1564\" alt=\"Screenshot
2024-10-31 at 16 11
31\"\r\nsrc=\"https://github.com/user-attachments/assets/e154ab5d-c5d4-4703-abd4-7bf3cd7a15fb\">\r\n\r\n\r\n###
Checklist\r\n\r\n- [x] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: Davis McPhee
<[email protected]>","sha":"c1e00a887173e8b86fd58fa0e7e768e4f0b1643c"}},{"branch":"8.16","label":"v8.16.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.x","label":"v8.17.0","branchLabelMappingKey":"^v8.17.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Julia Rechkunova <[email protected]>
  • Loading branch information
kibanamachine and jughosta authored Nov 6, 2024
1 parent 1557905 commit 3a3d0c1
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,20 @@ import { findTestSubject } from '@elastic/eui/lib/test';
import { EuiLoadingSpinner } from '@elastic/eui';
import { BehaviorSubject } from 'rxjs';
import { getDiscoverStateMock } from '../../__mocks__/discover_state.mock';
import { DataTotalHits$ } from '../../application/main/state_management/discover_data_state_container';
import {
DataDocuments$,
DataTotalHits$,
} from '../../application/main/state_management/discover_data_state_container';
import { FetchStatus } from '../../application/types';
import { dataViewMock, esHitsMock } from '@kbn/discover-utils/src/__mocks__';
import { buildDataTableRecord } from '@kbn/discover-utils';

function getDocuments$(count: number = 5) {
return new BehaviorSubject({
fetchStatus: FetchStatus.COMPLETE,
result: esHitsMock.map((esHit) => buildDataTableRecord(esHit, dataViewMock)).slice(0, count),
}) as DataDocuments$;
}

describe('hits counter', function () {
it('expect to render the number of hits', function () {
Expand All @@ -24,6 +36,7 @@ describe('hits counter', function () {
fetchStatus: FetchStatus.COMPLETE,
result: 1,
}) as DataTotalHits$;
stateContainer.dataState.data$.documents$ = getDocuments$();
const component1 = mountWithIntl(
<HitsCounter mode={HitsCounterMode.appended} stateContainer={stateContainer} />
);
Expand All @@ -45,6 +58,7 @@ describe('hits counter', function () {
fetchStatus: FetchStatus.COMPLETE,
result: 1899,
}) as DataTotalHits$;
stateContainer.dataState.data$.documents$ = getDocuments$();
const component1 = mountWithIntl(
<HitsCounter mode={HitsCounterMode.appended} stateContainer={stateContainer} />
);
Expand All @@ -64,6 +78,7 @@ describe('hits counter', function () {
fetchStatus: FetchStatus.PARTIAL,
result: 2,
}) as DataTotalHits$;
stateContainer.dataState.data$.documents$ = getDocuments$();
const component = mountWithIntl(
<HitsCounter mode={HitsCounterMode.standalone} stateContainer={stateContainer} />
);
Expand All @@ -76,6 +91,7 @@ describe('hits counter', function () {
fetchStatus: FetchStatus.PARTIAL,
result: 2,
}) as DataTotalHits$;
stateContainer.dataState.data$.documents$ = getDocuments$();
const component = mountWithIntl(
<HitsCounter mode={HitsCounterMode.standalone} stateContainer={stateContainer} />
);
Expand All @@ -89,9 +105,51 @@ describe('hits counter', function () {
fetchStatus: FetchStatus.LOADING,
result: undefined,
}) as DataTotalHits$;
stateContainer.dataState.data$.documents$ = getDocuments$();
const component = mountWithIntl(
<HitsCounter mode={HitsCounterMode.standalone} stateContainer={stateContainer} />
);
expect(component.isEmptyRender()).toBe(true);
});

it('should render discoverQueryHitsPartial when status is error', () => {
const stateContainer = getDiscoverStateMock({ isTimeBased: true });
stateContainer.dataState.data$.totalHits$ = new BehaviorSubject({
fetchStatus: FetchStatus.ERROR,
result: undefined,
}) as DataTotalHits$;
stateContainer.dataState.data$.documents$ = getDocuments$(3);
const component = mountWithIntl(
<HitsCounter mode={HitsCounterMode.standalone} stateContainer={stateContainer} />
);
expect(component.find('[data-test-subj="discoverQueryHitsPartial"]').length).toBe(1);
expect(findTestSubject(component, 'discoverQueryTotalHits').text()).toBe('≥3 resultsInfo');
expect(component.text()).toBe('≥3 resultsInfo');

stateContainer.dataState.data$.totalHits$ = new BehaviorSubject({
fetchStatus: FetchStatus.ERROR,
result: 200,
}) as DataTotalHits$;
stateContainer.dataState.data$.documents$ = getDocuments$(2);

const component2 = mountWithIntl(
<HitsCounter mode={HitsCounterMode.appended} stateContainer={stateContainer} />
);
expect(component2.find('[data-test-subj="discoverQueryHitsPartial"]').length).toBe(1);
expect(findTestSubject(component2, 'discoverQueryTotalHits').text()).toBe('≥200Info');
expect(component2.text()).toBe(' (≥200Info)');

stateContainer.dataState.data$.totalHits$ = new BehaviorSubject({
fetchStatus: FetchStatus.ERROR,
result: 0,
}) as DataTotalHits$;
stateContainer.dataState.data$.documents$ = getDocuments$(1);

const component3 = mountWithIntl(
<HitsCounter mode={HitsCounterMode.appended} stateContainer={stateContainer} />
);
expect(component3.find('[data-test-subj="discoverQueryHitsPartial"]').length).toBe(1);
expect(findTestSubject(component3, 'discoverQueryTotalHits').text()).toBe('≥1Info');
expect(component3.text()).toBe(' (≥1Info)');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
*/

import React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiText, EuiLoadingSpinner } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiText, EuiLoadingSpinner, EuiIconTip } from '@elastic/eui';
import { FormattedMessage, FormattedNumber } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { css } from '@emotion/react';
Expand All @@ -29,18 +29,33 @@ export interface HitsCounterProps {
export const HitsCounter: React.FC<HitsCounterProps> = ({ mode, stateContainer }) => {
const totalHits$ = stateContainer.dataState.data$.totalHits$;
const totalHitsState = useDataState(totalHits$);
const hitsTotal = totalHitsState.result;
let hitsTotal = totalHitsState.result;
const hitsStatus = totalHitsState.fetchStatus;

const documents$ = stateContainer.dataState.data$.documents$;
const documentsState = useDataState(documents$);
const documentsCount = documentsState.result?.length || 0;

if (!hitsTotal && hitsStatus === FetchStatus.LOADING) {
return null;
}

if (
hitsStatus === FetchStatus.ERROR &&
documentsState.fetchStatus === FetchStatus.COMPLETE &&
documentsCount > (hitsTotal ?? 0)
) {
// if histogram returned partial results and which are less than the fetched documents count =>
// override hitsTotal with the fetched documents count
hitsTotal = documentsCount;
}

const showGreaterOrEqualSign =
hitsStatus === FetchStatus.PARTIAL || hitsStatus === FetchStatus.ERROR;

const formattedHits = (
<span
data-test-subj={
hitsStatus === FetchStatus.PARTIAL ? 'discoverQueryHitsPartial' : 'discoverQueryHits'
}
data-test-subj={showGreaterOrEqualSign ? 'discoverQueryHitsPartial' : 'discoverQueryHits'}
>
<FormattedNumber value={hitsTotal ?? 0} />
</span>
Expand All @@ -55,7 +70,7 @@ export const HitsCounter: React.FC<HitsCounterProps> = ({ mode, stateContainer }

const element = (
<EuiFlexGroup
gutterSize="s"
gutterSize="xs"
responsive={false}
justifyContent="center"
alignItems="center"
Expand All @@ -66,8 +81,8 @@ export const HitsCounter: React.FC<HitsCounterProps> = ({ mode, stateContainer }
<EuiFlexItem grow={false} aria-live="polite" css={hitsCounterTextCss}>
<EuiText className="eui-textTruncate" size="s">
<strong>
{hitsStatus === FetchStatus.PARTIAL &&
(mode === HitsCounterMode.standalone ? (
{showGreaterOrEqualSign ? (
mode === HitsCounterMode.standalone ? (
<FormattedMessage
id="discover.hitsCounter.partialHitsPluralTitle"
defaultMessage="≥{formattedHits} {hits, plural, one {result} other {results}}"
Expand All @@ -79,17 +94,16 @@ export const HitsCounter: React.FC<HitsCounterProps> = ({ mode, stateContainer }
defaultMessage="≥{formattedHits}"
values={{ formattedHits }}
/>
))}
{hitsStatus !== FetchStatus.PARTIAL &&
(mode === HitsCounterMode.standalone ? (
<FormattedMessage
id="discover.hitsCounter.hitsPluralTitle"
defaultMessage="{formattedHits} {hits, plural, one {result} other {results}}"
values={{ hits: hitsTotal, formattedHits }}
/>
) : (
formattedHits
))}
)
) : mode === HitsCounterMode.standalone ? (
<FormattedMessage
id="discover.hitsCounter.hitsPluralTitle"
defaultMessage="{formattedHits} {hits, plural, one {result} other {results}}"
values={{ hits: hitsTotal, formattedHits }}
/>
) : (
formattedHits
)}
</strong>
</EuiText>
</EuiFlexItem>
Expand All @@ -103,6 +117,19 @@ export const HitsCounter: React.FC<HitsCounterProps> = ({ mode, stateContainer }
/>
</EuiFlexItem>
)}
{hitsStatus === FetchStatus.ERROR && (
<EuiFlexItem grow={false}>
<EuiIconTip
type="warning"
color="warning"
size="s"
content={i18n.translate('discover.hitsCounter.hitCountWarningTooltip', {
defaultMessage: 'Results might be incomplete',
})}
iconProps={{ css: { display: 'block' } }}
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ describe('Histogram', () => {
onLoad(false, adapters);
});
expect(props.onTotalHitsChange).toHaveBeenLastCalledWith(
UnifiedHistogramFetchStatus.complete,
UnifiedHistogramFetchStatus.error,
100
);
expect(props.onChartLoad).toHaveBeenLastCalledWith({ adapters });
Expand Down
15 changes: 8 additions & 7 deletions src/plugins/unified_histogram/public/chart/histogram.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,6 @@ export function Histogram({
| undefined;
const response = json?.rawResponse;

// The response can have `response?._shards.failed` but we should still be able to show hits number
// TODO: show shards warnings as a badge next to the total hits number

if (requestFailed) {
onTotalHitsChange?.(UnifiedHistogramFetchStatus.error, undefined);
onChartLoad?.({ adapters: adapters ?? {} });
Expand All @@ -142,10 +139,14 @@ export function Histogram({
const adapterTables = adapters?.tables?.tables;
const totalHits = computeTotalHits(hasLensSuggestions, adapterTables, isPlainRecord);

onTotalHitsChange?.(
isLoading ? UnifiedHistogramFetchStatus.loading : UnifiedHistogramFetchStatus.complete,
totalHits ?? hits?.total
);
if (response?._shards?.failed || response?.timed_out) {
onTotalHitsChange?.(UnifiedHistogramFetchStatus.error, totalHits);
} else {
onTotalHitsChange?.(
isLoading ? UnifiedHistogramFetchStatus.loading : UnifiedHistogramFetchStatus.complete,
totalHits ?? hits?.total
);
}

if (response) {
const newBucketInterval = buildBucketInterval({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {

// Ensure documents are still returned for the successful shards
await retry.try(async function tryingForTime() {
const hitCount = await discover.getHitCount();
const hitCount = await discover.getHitCount({ isPartial: true });
expect(hitCount).to.be('9,247');
});

Expand Down
6 changes: 4 additions & 2 deletions test/functional/page_objects/discover_page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,9 +338,11 @@ export class DiscoverPageObject extends FtrService {
return await this.header.waitUntilLoadingHasFinished();
}

public async getHitCount() {
public async getHitCount({ isPartial }: { isPartial?: boolean } = {}) {
await this.header.waitUntilLoadingHasFinished();
return await this.testSubjects.getVisibleText('discoverQueryHits');
return await this.testSubjects.getVisibleText(
isPartial ? 'discoverQueryHitsPartial' : 'discoverQueryHits'
);
}

public async getHitCountInt() {
Expand Down

0 comments on commit 3a3d0c1

Please sign in to comment.