Skip to content

Commit 2256919

Browse files
MichaelBuessemeyerMichael Büßemeyerdaniel-wer
authored
Some Timetracking view improvements (#8170)
* add option to select annotation state in time tracking overview * include annotation state in user specific time tracking csv download * show full long project names in timetracking filter input * fix tests * update timetracking snapshots * fix cyclic frontend dependencies * move AnnotationTypeFilterEnum and AnnotationStateFilterEnum to constants and increase constant width of timetracking filter dropdown * add missing changes of last commit * mend * Update frontend/javascripts/admin/statistic/project_and_annotation_type_dropdown.tsx Co-authored-by: Daniel <daniel.werner@scalableminds.com> --------- Co-authored-by: Michael Büßemeyer <frameworklinux+MichaelBuessemeyer@users.noreply.github.com> Co-authored-by: Daniel <daniel.werner@scalableminds.com>
1 parent f04daba commit 2256919

File tree

10 files changed

+117
-32
lines changed

10 files changed

+117
-32
lines changed

app/models/user/time/TimeSpan.scala

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ class TimeSpanDAO @Inject()(sqlClient: SqlClient)(implicit ec: ExecutionContext)
127127
val projectQuery = projectIdsFilterQuery(projectIds)
128128
for {
129129
tuples <- run(
130-
q"""SELECT ts._user, mu.email, o._id, d.name, a._id, t._id, p.name, tt._id, tt.summary, ts._id, ts.created, ts.time
130+
q"""SELECT ts._user, mu.email, o._id, d.name, a._id, a.state, t._id, p.name, tt._id, tt.summary, ts._id, ts.created, ts.time
131131
FROM webknossos.timespans_ ts
132132
JOIN webknossos.annotations_ a on ts._annotation = a._id
133133
JOIN webknossos.users_ u on ts._user = u._id
@@ -149,6 +149,7 @@ class TimeSpanDAO @Inject()(sqlClient: SqlClient)(implicit ec: ExecutionContext)
149149
String,
150150
String,
151151
String,
152+
String,
152153
Option[String],
153154
Option[String],
154155
Option[String],
@@ -165,6 +166,7 @@ class TimeSpanDAO @Inject()(sqlClient: SqlClient)(implicit ec: ExecutionContext)
165166
String,
166167
String,
167168
String,
169+
String,
168170
Option[String],
169171
Option[String],
170172
Option[String],
@@ -178,13 +180,14 @@ class TimeSpanDAO @Inject()(sqlClient: SqlClient)(implicit ec: ExecutionContext)
178180
"datasetOrganization" -> tuple._3,
179181
"datasetName" -> tuple._4,
180182
"annotationId" -> tuple._5,
181-
"taskId" -> tuple._6,
182-
"projectName" -> tuple._7,
183-
"taskTypeId" -> tuple._8,
184-
"taskTypeSummary" -> tuple._9,
185-
"timeSpanId" -> tuple._10,
186-
"timeSpanCreated" -> tuple._11,
187-
"timeSpanTimeMillis" -> tuple._12
183+
"annotationState" -> tuple._6,
184+
"taskId" -> tuple._7,
185+
"projectName" -> tuple._8,
186+
"taskTypeId" -> tuple._9,
187+
"taskTypeSummary" -> tuple._10,
188+
"timeSpanId" -> tuple._11,
189+
"timeSpanCreated" -> tuple._12,
190+
"timeSpanTimeMillis" -> tuple._13
188191
)
189192

190193
private def projectIdsFilterQuery(projectIds: List[ObjectId]): SqlToken =

frontend/javascripts/admin/admin_rest_api.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ import type {
6767
APITimeTrackingPerUser,
6868
} from "types/api_flow_types";
6969
import { APIAnnotationTypeEnum } from "types/api_flow_types";
70-
import type { LOG_LEVELS, Vector2, Vector3 } from "oxalis/constants";
71-
import Constants, { ControlModeEnum } from "oxalis/constants";
70+
import type { AnnotationTypeFilterEnum, LOG_LEVELS, Vector2, Vector3 } from "oxalis/constants";
71+
import Constants, { ControlModeEnum, AnnotationStateFilterEnum } from "oxalis/constants";
7272
import type {
7373
DatasetConfiguration,
7474
PartialDatasetConfiguration,
@@ -103,7 +103,6 @@ import { doWithToken } from "./api/token";
103103
import type BoundingBox from "oxalis/model/bucket_data_handling/bounding_box";
104104
import type { ArbitraryObject } from "types/globals";
105105
import { assertResponseLimit } from "./api/api_utils";
106-
import type { AnnotationTypeFilterEnum } from "admin/statistic/project_and_annotation_type_dropdown";
107106

108107
export * from "./api/token";
109108
export * from "./api/jobs";
@@ -1720,6 +1719,7 @@ export async function getTimeTrackingForUserSummedPerAnnotation(
17201719
startDate: dayjs.Dayjs,
17211720
endDate: dayjs.Dayjs,
17221721
annotationTypes: "Explorational" | "Task" | "Task,Explorational",
1722+
annotationState: AnnotationStateFilterEnum,
17231723
projectIds?: string[] | null,
17241724
): Promise<Array<APITimeTrackingPerAnnotation>> {
17251725
const params = new URLSearchParams({
@@ -1729,7 +1729,11 @@ export async function getTimeTrackingForUserSummedPerAnnotation(
17291729
if (annotationTypes != null) params.append("annotationTypes", annotationTypes);
17301730
if (projectIds != null && projectIds.length > 0)
17311731
params.append("projectIds", projectIds.join(","));
1732-
params.append("annotationStates", "Active,Finished");
1732+
if (annotationState !== AnnotationStateFilterEnum.ALL) {
1733+
params.append("annotationStates", annotationState);
1734+
} else {
1735+
params.append("annotationStates", "Active,Finished");
1736+
}
17331737
const timeTrackingData = await Request.receiveJSON(
17341738
`/api/time/user/${userId}/summedByAnnotation?${params}`,
17351739
);
@@ -1742,16 +1746,22 @@ export async function getTimeTrackingForUserSpans(
17421746
startDate: number,
17431747
endDate: number,
17441748
annotationTypes: "Explorational" | "Task" | "Task,Explorational",
1749+
selectedState: AnnotationStateFilterEnum,
17451750
projectIds?: string[] | null,
17461751
): Promise<Array<APITimeTrackingSpan>> {
17471752
const params = new URLSearchParams({
17481753
start: startDate.toString(),
17491754
end: endDate.toString(),
17501755
});
17511756
if (annotationTypes != null) params.append("annotationTypes", annotationTypes);
1752-
if (projectIds != null && projectIds.length > 0)
1757+
if (projectIds != null && projectIds.length > 0) {
17531758
params.append("projectIds", projectIds.join(","));
1754-
params.append("annotationStates", "Active,Finished");
1759+
}
1760+
if (selectedState !== AnnotationStateFilterEnum.ALL) {
1761+
params.append("annotationStates", selectedState);
1762+
} else {
1763+
params.append("annotationStates", "Active,Finished");
1764+
}
17551765
return await Request.receiveJSON(`/api/time/user/${userId}/spans?${params}`);
17561766
}
17571767

@@ -1760,17 +1770,22 @@ export async function getTimeEntries(
17601770
endMs: number,
17611771
teamIds: string[],
17621772
selectedTypes: AnnotationTypeFilterEnum,
1773+
selectedState: AnnotationStateFilterEnum,
17631774
projectIds: string[],
17641775
): Promise<Array<APITimeTrackingPerUser>> {
17651776
const params = new URLSearchParams({
17661777
start: startMs.toString(),
17671778
end: endMs.toString(),
17681779
annotationTypes: selectedTypes,
17691780
});
1781+
if (selectedState !== AnnotationStateFilterEnum.ALL) {
1782+
params.append("annotationStates", selectedState);
1783+
} else {
1784+
params.append("annotationStates", "Active,Finished");
1785+
}
17701786
// Omit empty parameters in request
17711787
if (projectIds.length > 0) params.append("projectIds", projectIds.join(","));
17721788
if (teamIds.length > 0) params.append("teamIds", teamIds.join(","));
1773-
params.append("annotationStates", "Active,Finished");
17741789
return await Request.receiveJSON(`api/time/overview?${params}`);
17751790
}
17761791

frontend/javascripts/admin/statistic/project_and_annotation_type_dropdown.tsx

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,16 @@ import { useFetch } from "libs/react_helpers";
66
import { isUserAdminOrTeamManager } from "libs/utils";
77
import { useSelector } from "react-redux";
88
import type { OxalisState } from "oxalis/store";
9-
10-
export enum AnnotationTypeFilterEnum {
11-
ONLY_ANNOTATIONS_KEY = "Explorational",
12-
ONLY_TASKS_KEY = "Task",
13-
TASKS_AND_ANNOTATIONS_KEY = "Task,Explorational",
14-
}
9+
import { AnnotationStateFilterEnum, AnnotationTypeFilterEnum } from "oxalis/constants";
1510

1611
type ProjectAndTypeDropdownProps = {
1712
selectedProjectIds: string[];
1813
setSelectedProjectIds: (projectIds: string[]) => void;
1914
selectedAnnotationType: AnnotationTypeFilterEnum;
2015
setSelectedAnnotationType: (type: AnnotationTypeFilterEnum) => void;
16+
selectedAnnotationState: string;
17+
setSelectedAnnotationState: (state: AnnotationStateFilterEnum) => void;
18+
2119
style?: React.CSSProperties;
2220
};
2321

@@ -38,11 +36,21 @@ const ANNOTATION_TYPE_FILTERS: NestedSelectOptions = {
3836
],
3937
};
4038

39+
const ANNOTATION_STATE_FILTERS: NestedSelectOptions = {
40+
label: "Filter by state",
41+
options: [
42+
{ label: "Active", value: AnnotationStateFilterEnum.ACTIVE },
43+
{ label: "Finished / Archived", value: AnnotationStateFilterEnum.FINISHED_OR_ARCHIVED },
44+
],
45+
};
46+
4147
function ProjectAndAnnotationTypeDropdown({
4248
selectedProjectIds,
4349
setSelectedProjectIds,
4450
selectedAnnotationType,
4551
setSelectedAnnotationType,
52+
selectedAnnotationState,
53+
setSelectedAnnotationState,
4654
style,
4755
}: ProjectAndTypeDropdownProps) {
4856
// This state property is derived from selectedProjectIds and selectedAnnotationType.
@@ -60,12 +68,14 @@ function ProjectAndAnnotationTypeDropdown({
6068
);
6169

6270
useEffect(() => {
71+
const selectedKeys =
72+
selectedAnnotationState !== AnnotationStateFilterEnum.ALL ? [selectedAnnotationState] : [];
6373
if (selectedProjectIds.length > 0) {
64-
setSelectedFilters(selectedProjectIds);
74+
setSelectedFilters([...selectedProjectIds, ...selectedKeys]);
6575
} else {
66-
setSelectedFilters([selectedAnnotationType]);
76+
setSelectedFilters([selectedAnnotationType, ...selectedKeys]);
6777
}
68-
}, [selectedProjectIds, selectedAnnotationType]);
78+
}, [selectedProjectIds, selectedAnnotationType, selectedAnnotationState]);
6979

7080
useEffect(() => {
7181
const projectOptions = allProjects.map((project) => {
@@ -74,14 +84,18 @@ function ProjectAndAnnotationTypeDropdown({
7484
value: project.id,
7585
};
7686
});
77-
let allOptions = [ANNOTATION_TYPE_FILTERS];
87+
let allOptions = [ANNOTATION_TYPE_FILTERS, ANNOTATION_STATE_FILTERS];
7888
if (projectOptions.length > 0) {
7989
allOptions.push({ label: "Filter projects (only tasks)", options: projectOptions });
8090
}
8191
setFilterOptions(allOptions);
8292
}, [allProjects]);
8393

8494
const setSelectedProjects = async (_prevSelection: string[], selectedValue: string) => {
95+
if (Object.values<string>(AnnotationStateFilterEnum).includes(selectedValue)) {
96+
setSelectedAnnotationState(selectedValue as AnnotationStateFilterEnum);
97+
return;
98+
}
8599
if (Object.values<string>(AnnotationTypeFilterEnum).includes(selectedValue)) {
86100
setSelectedAnnotationType(selectedValue as AnnotationTypeFilterEnum);
87101
setSelectedProjectIds([]);
@@ -94,6 +108,8 @@ function ProjectAndAnnotationTypeDropdown({
94108
const onDeselect = (removedKey: string) => {
95109
if (Object.values<string>(AnnotationTypeFilterEnum).includes(removedKey)) {
96110
setSelectedAnnotationType(AnnotationTypeFilterEnum.TASKS_AND_ANNOTATIONS_KEY);
111+
} else if (Object.values<string>(AnnotationStateFilterEnum).includes(removedKey)) {
112+
setSelectedAnnotationState(AnnotationStateFilterEnum.ALL);
97113
} else {
98114
setSelectedProjectIds(selectedProjectIds.filter((projectId) => projectId !== removedKey));
99115
}
@@ -108,6 +124,7 @@ function ProjectAndAnnotationTypeDropdown({
108124
options={filterOptions}
109125
optionFilterProp="label"
110126
value={selectedFilters}
127+
popupMatchSelectWidth={400}
111128
onDeselect={(removedKey: string) => onDeselect(removedKey)}
112129
onSelect={(newSelection: string) => setSelectedProjects(selectedFilters, newSelection)}
113130
/>

frontend/javascripts/admin/statistic/time_tracking_detail_view.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { useFetch } from "libs/react_helpers";
2-
import type { AnnotationTypeFilterEnum } from "./project_and_annotation_type_dropdown";
32
import { getTimeTrackingForUserSummedPerAnnotation } from "admin/admin_rest_api";
43
import dayjs from "dayjs";
54
import { Col, Divider, Row } from "antd";
@@ -8,11 +7,13 @@ import _ from "lodash";
87
import type { APITimeTrackingPerAnnotation } from "types/api_flow_types";
98
import { AnnotationStats } from "oxalis/view/right-border-tabs/dataset_info_tab_view";
109
import { aggregateStatsForAllLayers } from "oxalis/model/accessors/annotation_accessor";
10+
import type { AnnotationTypeFilterEnum, AnnotationStateFilterEnum } from "oxalis/constants";
1111

1212
type TimeTrackingDetailViewProps = {
1313
userId: string;
1414
dateRange: [number, number];
1515
annotationType: AnnotationTypeFilterEnum;
16+
annotationState: AnnotationStateFilterEnum;
1617
projectIds: string[];
1718
};
1819

@@ -84,6 +85,7 @@ function TimeTrackingDetailView(props: TimeTrackingDetailViewProps) {
8485
dayjs(props.dateRange[0]),
8586
dayjs(props.dateRange[1]),
8687
props.annotationType,
88+
props.annotationState,
8789
props.projectIds,
8890
);
8991
},

frontend/javascripts/admin/statistic/time_tracking_overview.tsx

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@ import { useState } from "react";
55
import { DownloadOutlined, FilterOutlined } from "@ant-design/icons";
66
import saveAs from "file-saver";
77
import { formatMilliseconds } from "libs/format_utils";
8-
import ProjectAndAnnotationTypeDropdown, {
9-
AnnotationTypeFilterEnum,
10-
} from "./project_and_annotation_type_dropdown";
8+
import ProjectAndAnnotationTypeDropdown from "./project_and_annotation_type_dropdown";
119
import { isUserAdminOrTeamManager, transformToCSVRow } from "libs/utils";
1210
import messages from "messages";
1311
import Toast from "libs/toast";
@@ -19,11 +17,12 @@ import type { APITimeTrackingPerUser } from "types/api_flow_types";
1917
import { useSelector } from "react-redux";
2018
import type { OxalisState } from "oxalis/store";
2119
import dayjs, { type Dayjs } from "dayjs";
20+
import { AnnotationTypeFilterEnum, AnnotationStateFilterEnum } from "oxalis/constants";
2221
const { RangePicker } = DatePicker;
2322

2423
const TIMETRACKING_CSV_HEADER_PER_USER = ["userId,userFirstName,userLastName,timeTrackedInSeconds"];
2524
const TIMETRACKING_CSV_HEADER_SPANS = [
26-
"userId,email,datasetOrga,datasetName,annotation,startTimeUnixTimestamp,durationInSeconds,taskId,projectName,taskTypeId,taskTypeSummary",
25+
"userId,email,datasetOrga,datasetName,annotation,annotationState,startTimeUnixTimestamp,durationInSeconds,taskId,projectName,taskTypeId,taskTypeSummary",
2726
];
2827

2928
function TimeTrackingOverview() {
@@ -50,6 +49,7 @@ function TimeTrackingOverview() {
5049
const [selectedTypes, setSelectedTypes] = useState(
5150
AnnotationTypeFilterEnum.TASKS_AND_ANNOTATIONS_KEY,
5251
);
52+
const [selectedState, setSelectedState] = useState(AnnotationStateFilterEnum.ALL);
5353
const [selectedTeams, setSelectedTeams] = useState(allTeams.map((team) => team.id));
5454
const filteredTimeEntries = useFetch(
5555
async () => {
@@ -59,13 +59,14 @@ function TimeTrackingOverview() {
5959
endDate.valueOf(),
6060
selectedTeams,
6161
selectedTypes,
62+
selectedState,
6263
selectedProjectIds,
6364
);
6465
setIsFetching(false);
6566
return filteredEntries;
6667
},
6768
[],
68-
[selectedTeams, selectedTypes, selectedProjectIds, startDate, endDate],
69+
[selectedTeams, selectedTypes, selectedState, selectedProjectIds, startDate, endDate],
6970
);
7071
const filterStyle = { marginInline: 10 };
7172

@@ -74,13 +75,15 @@ function TimeTrackingOverview() {
7475
start: Dayjs,
7576
end: Dayjs,
7677
annotationTypes: AnnotationTypeFilterEnum,
78+
selectedState: AnnotationStateFilterEnum,
7779
projectIds: string[] | null | undefined,
7880
) => {
7981
const timeSpans = await getTimeTrackingForUserSpans(
8082
userId,
8183
start.valueOf(),
8284
end.valueOf(),
8385
annotationTypes,
86+
selectedState,
8487
projectIds,
8588
);
8689
const timeEntriesAsString = timeSpans
@@ -91,6 +94,7 @@ function TimeTrackingOverview() {
9194
row.datasetOrganization,
9295
row.datasetName,
9396
row.annotationId,
97+
row.annotationState,
9498
row.timeSpanCreated,
9599
Math.ceil(row.timeSpanTimeMillis / 1000),
96100
row.taskId,
@@ -186,7 +190,14 @@ function TimeTrackingOverview() {
186190
return (
187191
<LinkButton
188192
onClick={async () => {
189-
downloadTimeSpans(user.id, startDate, endDate, selectedTypes, selectedProjectIds);
193+
downloadTimeSpans(
194+
user.id,
195+
startDate,
196+
endDate,
197+
selectedTypes,
198+
selectedState,
199+
selectedProjectIds,
200+
);
190201
}}
191202
>
192203
<DownloadOutlined className="icon-margin-right" />
@@ -239,6 +250,8 @@ function TimeTrackingOverview() {
239250
selectedProjectIds={selectedProjectIds}
240251
setSelectedAnnotationType={setSelectedTypes}
241252
selectedAnnotationType={selectedTypes}
253+
selectedAnnotationState={selectedState}
254+
setSelectedAnnotationState={setSelectedState}
242255
style={{ ...filterStyle }}
243256
/>
244257
<Select
@@ -289,6 +302,7 @@ function TimeTrackingOverview() {
289302
userId={entry.user.id}
290303
dateRange={[startDate.valueOf(), endDate.valueOf()]}
291304
annotationType={selectedTypes}
305+
annotationState={selectedState}
292306
projectIds={selectedProjectIds}
293307
/>
294308
),

frontend/javascripts/oxalis/constants.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,3 +486,15 @@ export const LongUnitToShortUnitMap: Record<UnitLong, UnitShort> = {
486486
};
487487

488488
export const AllUnits = Object.values(UnitLong);
489+
490+
export enum AnnotationTypeFilterEnum {
491+
ONLY_ANNOTATIONS_KEY = "Explorational",
492+
ONLY_TASKS_KEY = "Task",
493+
TASKS_AND_ANNOTATIONS_KEY = "Task,Explorational",
494+
}
495+
496+
export enum AnnotationStateFilterEnum {
497+
ALL = "All",
498+
ACTIVE = "Active",
499+
FINISHED_OR_ARCHIVED = "Finished",
500+
}

0 commit comments

Comments
 (0)