Skip to content

Commit 8fb1229

Browse files
authored
Merge pull request #2524 from github/koesie10/grouping-improvements
Improve grouping of libraries
2 parents 23173bf + 2800ccb commit 8fb1229

File tree

9 files changed

+249
-52
lines changed

9 files changed

+249
-52
lines changed

extensions/ql-vscode/src/data-extensions-editor/bqrs.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -47,17 +47,5 @@ export function decodeBqrsToExternalApiUsages(
4747
method.usages.push(usage);
4848
});
4949

50-
const externalApiUsages = Array.from(methodsByApiName.values());
51-
externalApiUsages.sort((a, b) => {
52-
// Sort first by supported, putting unmodeled methods first.
53-
if (a.supported && !b.supported) {
54-
return 1;
55-
}
56-
if (!a.supported && b.supported) {
57-
return -1;
58-
}
59-
// Then sort by number of usages descending
60-
return b.usages.length - a.usages.length;
61-
});
62-
return externalApiUsages;
50+
return Array.from(methodsByApiName.values());
6351
}

extensions/ql-vscode/src/pure/word.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ export function pluralize(
77
numItems: number | undefined,
88
singular: string,
99
plural: string,
10+
numberFormatter: (value: number) => string = (value) => value.toString(),
1011
): string {
1112
return numItems !== undefined
12-
? `${numItems} ${numItems === 1 ? singular : plural}`
13+
? `${numberFormatter(numItems)} ${numItems === 1 ? singular : plural}`
1314
: "";
1415
}

extensions/ql-vscode/src/view/data-extensions-editor/DataExtensionsEditor.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { basename } from "../common/path";
1616
import { ViewTitle } from "../common";
1717
import { DataExtensionEditorViewState } from "../../data-extensions-editor/shared/view-state";
1818
import { ModeledMethodsList } from "./ModeledMethodsList";
19+
import { percentFormatter } from "./formatters";
1920

2021
const DataExtensionsEditorContainer = styled.div`
2122
margin-top: 1rem;
@@ -213,8 +214,12 @@ export function DataExtensionsEditor({
213214
)}
214215
</>
215216
)}
216-
<div>{modeledPercentage.toFixed(2)}% modeled</div>
217-
<div>{unModeledPercentage.toFixed(2)}% unmodeled</div>
217+
<div>
218+
{percentFormatter.format(modeledPercentage / 100)} modeled
219+
</div>
220+
<div>
221+
{percentFormatter.format(unModeledPercentage / 100)} unmodeled
222+
</div>
218223
</DetailsContainer>
219224

220225
<EditorContainer>
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import * as React from "react";
2+
import { useCallback, useMemo, useState } from "react";
3+
import styled from "styled-components";
4+
import { ExternalApiUsage } from "../../data-extensions-editor/external-api-usage";
5+
import { ModeledMethod } from "../../data-extensions-editor/modeled-method";
6+
import { pluralize } from "../../pure/word";
7+
import { ModeledMethodDataGrid } from "./ModeledMethodDataGrid";
8+
import { calculateModeledPercentage } from "./modeled";
9+
import { decimalFormatter, percentFormatter } from "./formatters";
10+
import { Codicon } from "../common";
11+
12+
const LibraryContainer = styled.div`
13+
margin-bottom: 1rem;
14+
`;
15+
16+
const TitleContainer = styled.button`
17+
display: flex;
18+
gap: 0.5em;
19+
align-items: center;
20+
width: 100%;
21+
font-size: 1.2em;
22+
font-weight: bold;
23+
24+
color: var(--vscode-editor-foreground);
25+
background-color: transparent;
26+
border: none;
27+
cursor: pointer;
28+
`;
29+
30+
const StatusContainer = styled.div`
31+
display: flex;
32+
gap: 1em;
33+
align-items: center;
34+
35+
margin-top: 0.5em;
36+
margin-bottom: 0.5em;
37+
margin-left: 1em;
38+
`;
39+
40+
type Props = {
41+
libraryName: string;
42+
externalApiUsages: ExternalApiUsage[];
43+
modeledMethods: Record<string, ModeledMethod>;
44+
onChange: (
45+
externalApiUsage: ExternalApiUsage,
46+
modeledMethod: ModeledMethod,
47+
) => void;
48+
};
49+
50+
export const LibraryRow = ({
51+
libraryName,
52+
externalApiUsages,
53+
modeledMethods,
54+
onChange,
55+
}: Props) => {
56+
const modeledPercentage = useMemo(() => {
57+
return calculateModeledPercentage(externalApiUsages);
58+
}, [externalApiUsages]);
59+
60+
const [isExpanded, setExpanded] = useState(modeledPercentage < 100);
61+
62+
const toggleExpanded = useCallback(async () => {
63+
setExpanded((oldIsExpanded) => !oldIsExpanded);
64+
}, []);
65+
66+
const usagesCount = useMemo(() => {
67+
return externalApiUsages.reduce((acc, curr) => acc + curr.usages.length, 0);
68+
}, [externalApiUsages]);
69+
70+
return (
71+
<LibraryContainer>
72+
<TitleContainer onClick={toggleExpanded} aria-expanded={isExpanded}>
73+
{isExpanded ? (
74+
<Codicon name="chevron-down" label="Collapse" />
75+
) : (
76+
<Codicon name="chevron-right" label="Expand" />
77+
)}
78+
{libraryName}
79+
{isExpanded ? null : (
80+
<>
81+
{" "}
82+
(
83+
{pluralize(
84+
externalApiUsages.length,
85+
"method",
86+
"methods",
87+
decimalFormatter.format.bind(decimalFormatter),
88+
)}
89+
, {percentFormatter.format(modeledPercentage / 100)} modeled)
90+
</>
91+
)}
92+
</TitleContainer>
93+
{isExpanded && (
94+
<>
95+
<StatusContainer>
96+
<div>
97+
{pluralize(
98+
externalApiUsages.length,
99+
"method",
100+
"methods",
101+
decimalFormatter.format.bind(decimalFormatter),
102+
)}
103+
</div>
104+
<div>
105+
{pluralize(
106+
usagesCount,
107+
"usage",
108+
"usages",
109+
decimalFormatter.format.bind(decimalFormatter),
110+
)}
111+
</div>
112+
<div>
113+
{percentFormatter.format(modeledPercentage / 100)} modeled
114+
</div>
115+
</StatusContainer>
116+
<ModeledMethodDataGrid
117+
externalApiUsages={externalApiUsages}
118+
modeledMethods={modeledMethods}
119+
onChange={onChange}
120+
/>
121+
</>
122+
)}
123+
</LibraryContainer>
124+
);
125+
};

extensions/ql-vscode/src/view/data-extensions-editor/ModeledMethodDataGrid.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
import { MethodRow } from "./MethodRow";
88
import { ExternalApiUsage } from "../../data-extensions-editor/external-api-usage";
99
import { ModeledMethod } from "../../data-extensions-editor/modeled-method";
10+
import { useMemo } from "react";
1011

1112
type Props = {
1213
externalApiUsages: ExternalApiUsage[];
@@ -22,6 +23,22 @@ export const ModeledMethodDataGrid = ({
2223
modeledMethods,
2324
onChange,
2425
}: Props) => {
26+
const sortedExternalApiUsages = useMemo(() => {
27+
const sortedExternalApiUsages = [...externalApiUsages];
28+
sortedExternalApiUsages.sort((a, b) => {
29+
// Sort first by supported, putting unmodeled methods first.
30+
if (a.supported && !b.supported) {
31+
return 1;
32+
}
33+
if (!a.supported && b.supported) {
34+
return -1;
35+
}
36+
// Then sort by number of usages descending
37+
return b.usages.length - a.usages.length;
38+
});
39+
return sortedExternalApiUsages;
40+
}, [externalApiUsages]);
41+
2542
return (
2643
<VSCodeDataGrid>
2744
<VSCodeDataGridRow rowType="header">
@@ -47,7 +64,7 @@ export const ModeledMethodDataGrid = ({
4764
Kind
4865
</VSCodeDataGridCell>
4966
</VSCodeDataGridRow>
50-
{externalApiUsages.map((externalApiUsage) => (
67+
{sortedExternalApiUsages.map((externalApiUsage) => (
5168
<MethodRow
5269
key={externalApiUsage.signature}
5370
externalApiUsage={externalApiUsage}

extensions/ql-vscode/src/view/data-extensions-editor/ModeledMethodsList.tsx

Lines changed: 50 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
import * as React from "react";
22
import { useMemo } from "react";
3-
import styled from "styled-components";
43
import { ExternalApiUsage } from "../../data-extensions-editor/external-api-usage";
54
import { ModeledMethod } from "../../data-extensions-editor/modeled-method";
6-
import { ModeledMethodDataGrid } from "./ModeledMethodDataGrid";
7-
8-
const LibraryContainer = styled.div`
9-
margin-bottom: 1rem;
10-
`;
5+
import { calculateModeledPercentage } from "./modeled";
6+
import { LibraryRow } from "./LibraryRow";
117

128
type Props = {
139
externalApiUsages: ExternalApiUsage[];
@@ -35,20 +31,59 @@ export const ModeledMethodsList = ({
3531
}, [externalApiUsages]);
3632

3733
const sortedLibraryNames = useMemo(() => {
38-
return Object.keys(groupedByLibrary).sort();
34+
return Object.keys(groupedByLibrary).sort((a, b) => {
35+
const supportedPercentageA = calculateModeledPercentage(
36+
groupedByLibrary[a],
37+
);
38+
const supportedPercentageB = calculateModeledPercentage(
39+
groupedByLibrary[b],
40+
);
41+
42+
// Sort first by supported percentage ascending
43+
if (supportedPercentageA > supportedPercentageB) {
44+
return 1;
45+
}
46+
if (supportedPercentageA < supportedPercentageB) {
47+
return -1;
48+
}
49+
50+
const numberOfUsagesA = groupedByLibrary[a].reduce(
51+
(acc, curr) => acc + curr.usages.length,
52+
0,
53+
);
54+
const numberOfUsagesB = groupedByLibrary[b].reduce(
55+
(acc, curr) => acc + curr.usages.length,
56+
0,
57+
);
58+
59+
// If the number of usages is equal, sort by number of methods descending
60+
if (numberOfUsagesA === numberOfUsagesB) {
61+
const numberOfMethodsA = groupedByLibrary[a].length;
62+
const numberOfMethodsB = groupedByLibrary[b].length;
63+
64+
// If the number of methods is equal, sort by library name ascending
65+
if (numberOfMethodsA === numberOfMethodsB) {
66+
return a.localeCompare(b);
67+
}
68+
69+
return numberOfMethodsB - numberOfMethodsA;
70+
}
71+
72+
// Then sort by number of usages descending
73+
return numberOfUsagesB - numberOfUsagesA;
74+
});
3975
}, [groupedByLibrary]);
4076

4177
return (
4278
<>
4379
{sortedLibraryNames.map((libraryName) => (
44-
<LibraryContainer key={libraryName}>
45-
<h3>{libraryName}</h3>
46-
<ModeledMethodDataGrid
47-
externalApiUsages={groupedByLibrary[libraryName]}
48-
modeledMethods={modeledMethods}
49-
onChange={onChange}
50-
/>
51-
</LibraryContainer>
80+
<LibraryRow
81+
key={libraryName}
82+
libraryName={libraryName}
83+
externalApiUsages={groupedByLibrary[libraryName]}
84+
modeledMethods={modeledMethods}
85+
onChange={onChange}
86+
/>
5287
))}
5388
</>
5489
);
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export const decimalFormatter = new Intl.NumberFormat("en-US", {
2+
style: "decimal",
3+
maximumFractionDigits: 2,
4+
});
5+
6+
export const percentFormatter = new Intl.NumberFormat("en-US", {
7+
style: "percent",
8+
maximumFractionDigits: 2,
9+
});

extensions/ql-vscode/test/unit-tests/data-extensions-editor/bqrs.test.ts

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,26 @@ describe("decodeBqrsToExternalApiUsages", () => {
168168
// - Iterating over a map (as done by .values()) is guaranteed to be in insertion order
169169
// - Sorting the array of usages is guaranteed to be a stable sort
170170
expect(decodeBqrsToExternalApiUsages(chunk)).toEqual([
171+
{
172+
signature: "java.io.PrintStream#println(String)",
173+
packageName: "java.io",
174+
typeName: "PrintStream",
175+
methodName: "println",
176+
methodParameters: "(String)",
177+
supported: true,
178+
usages: [
179+
{
180+
label: "println(...)",
181+
url: {
182+
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
183+
startLine: 29,
184+
startColumn: 9,
185+
endLine: 29,
186+
endColumn: 49,
187+
},
188+
},
189+
],
190+
},
171191
{
172192
signature:
173193
"org.springframework.boot.SpringApplication#run(Class,String[])",
@@ -279,26 +299,6 @@ describe("decodeBqrsToExternalApiUsages", () => {
279299
},
280300
],
281301
},
282-
{
283-
signature: "java.io.PrintStream#println(String)",
284-
packageName: "java.io",
285-
typeName: "PrintStream",
286-
methodName: "println",
287-
methodParameters: "(String)",
288-
supported: true,
289-
usages: [
290-
{
291-
label: "println(...)",
292-
url: {
293-
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
294-
startLine: 29,
295-
startColumn: 9,
296-
endLine: 29,
297-
endColumn: 49,
298-
},
299-
},
300-
],
301-
},
302302
{
303303
signature: "org.sql2o.Sql2o#Sql2o(String,String,String)",
304304
packageName: "org.sql2o",

extensions/ql-vscode/test/unit-tests/pure/word.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,22 @@ describe("word helpers", () => {
1414
it("should return the empty string if the number is undefined", () => {
1515
expect(pluralize(undefined, "thing", "things")).toBe("");
1616
});
17+
it("should return an unformatted number when no formatter is specified", () => {
18+
expect(pluralize(1_000_000, "thing", "things")).toBe("1000000 things");
19+
});
20+
it("should return a formatted number when a formatter is specified", () => {
21+
const formatter = new Intl.NumberFormat("en-US", {
22+
style: "decimal",
23+
});
24+
25+
expect(
26+
pluralize(
27+
1_000_000,
28+
"thing",
29+
"things",
30+
formatter.format.bind(formatter),
31+
),
32+
).toBe("1,000,000 things");
33+
});
1734
});
1835
});

0 commit comments

Comments
 (0)