Skip to content

Commit 7663f76

Browse files
authored
Artefact signoff frontend (#90)
* Refactor big file into smaller files * Store artefact status * Add UI for artefact sign off * Add artefact status chip on artefact cards * Add frontend functionality to change artefact status * Return updated artefact on patch endpoint * Pass correct artefact status value * fix order of returned artefacts
1 parent 0f99704 commit 7663f76

File tree

14 files changed

+289
-122
lines changed

14 files changed

+289
-122
lines changed

backend/test_observer/controllers/artefacts/artefacts.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,23 @@
3434
def get_artefacts(family: FamilyName | None = None, db: Session = Depends(get_db)):
3535
"""Get latest artefacts optionally by family"""
3636
artefacts = []
37+
order_by = (Artefact.name, Artefact.created_at)
3738

3839
if family:
39-
artefacts = get_artefacts_by_family(db, family, load_stage=True)
40+
artefacts = get_artefacts_by_family(
41+
db,
42+
family,
43+
load_stage=True,
44+
order_by_columns=order_by,
45+
)
4046
else:
4147
for family in FamilyName:
42-
artefacts += get_artefacts_by_family(db, family, load_stage=True)
48+
artefacts += get_artefacts_by_family(
49+
db,
50+
family,
51+
load_stage=True,
52+
order_by_columns=order_by,
53+
)
4354

4455
return artefacts
4556

@@ -55,7 +66,7 @@ def get_artefact(artefact_id: int, db: Session = Depends(get_db)):
5566
return artefact
5667

5768

58-
@router.patch("/{artefact_id}")
69+
@router.patch("/{artefact_id}", response_model=ArtefactDTO)
5970
def patch_artefact(
6071
artefact_id: int, request: ArtefactPatch, db: Session = Depends(get_db)
6172
):
@@ -67,6 +78,8 @@ def patch_artefact(
6778
artefact.status = request.status
6879
db.commit()
6980

81+
return artefact
82+
7083

7184
@router.get("/{artefact_id}/builds", response_model=list[ArtefactBuildDTO])
7285
def get_artefact_builds(artefact_id: int, db: Session = Depends(get_db)):

backend/test_observer/data_access/repository.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
"""Services for working with objects from DB"""
2121

2222

23+
from collections.abc import Iterable
24+
from typing import Any
25+
2326
from sqlalchemy import and_, func
2427
from sqlalchemy.exc import IntegrityError
2528
from sqlalchemy.orm import Session, joinedload
@@ -52,6 +55,7 @@ def get_artefacts_by_family(
5255
family_name: FamilyName,
5356
latest_only: bool = True,
5457
load_stage: bool = False,
58+
order_by_columns: Iterable[Any] | None = None,
5559
) -> list[Artefact]:
5660
"""
5761
Get all the artefacts
@@ -120,6 +124,9 @@ def get_artefacts_by_family(
120124
if load_stage:
121125
query = query.options(joinedload(Artefact.stage))
122126

127+
if order_by_columns:
128+
query = query.order_by(*order_by_columns)
129+
123130
return query.all()
124131

125132

backend/tests/controllers/artefacts/test_artefacts.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,14 @@ def test_artefact_signoff(db_session: Session, test_client: TestClient):
155155

156156
assert response.status_code == 200
157157
assert artefact.status == ArtefactStatus.APPROVED
158+
assert response.json() == {
159+
"id": artefact.id,
160+
"name": artefact.name,
161+
"version": artefact.version,
162+
"track": artefact.track,
163+
"store": artefact.store,
164+
"series": artefact.series,
165+
"repo": artefact.repo,
166+
"stage": artefact.stage.name,
167+
"status": artefact.status,
168+
}

frontend/lib/models/artefact.dart

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import 'package:flutter/material.dart';
12
import 'package:freezed_annotation/freezed_annotation.dart';
3+
import 'package:yaru/yaru.dart';
24

35
import 'stage_name.dart';
46

@@ -17,6 +19,7 @@ class Artefact with _$Artefact {
1719
@Default(null) String? store,
1820
@Default(null) String? series,
1921
@Default(null) String? repo,
22+
required ArtefactStatus status,
2023
required StageName stage,
2124
}) = _Artefact;
2225

@@ -31,3 +34,45 @@ class Artefact with _$Artefact {
3134
if (repo != null) 'repo': repo!,
3235
};
3336
}
37+
38+
enum ArtefactStatus {
39+
@JsonValue('APPROVED')
40+
approved,
41+
@JsonValue('MARKED_AS_FAILED')
42+
rejected,
43+
@JsonValue('UNDECIDED')
44+
undecided;
45+
46+
String get name {
47+
switch (this) {
48+
case approved:
49+
return 'Approved';
50+
case rejected:
51+
return 'Rejected';
52+
case undecided:
53+
return 'Undecided';
54+
}
55+
}
56+
57+
Color get color {
58+
switch (this) {
59+
case approved:
60+
return YaruColors.light.success;
61+
case rejected:
62+
return YaruColors.red;
63+
case undecided:
64+
return YaruColors.textGrey;
65+
}
66+
}
67+
68+
String toJson() {
69+
switch (this) {
70+
case approved:
71+
return 'APPROVED';
72+
case rejected:
73+
return 'MARKED_AS_FAILED';
74+
case undecided:
75+
return 'UNDECIDED';
76+
}
77+
}
78+
}

frontend/lib/providers/artefact.dart

Lines changed: 0 additions & 15 deletions
This file was deleted.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import 'package:riverpod_annotation/riverpod_annotation.dart';
2+
3+
import '../models/artefact.dart';
4+
import 'dio.dart';
5+
6+
part 'artefact_notifier.g.dart';
7+
8+
@riverpod
9+
class ArtefactNotifier extends _$ArtefactNotifier {
10+
@override
11+
Future<Artefact> build(int artefactId) async {
12+
final dio = ref.watch(dioProvider);
13+
14+
final response = await dio.get('/v1/artefacts/$artefactId');
15+
final artefact = Artefact.fromJson(response.data);
16+
return artefact;
17+
}
18+
19+
Future<void> changeStatus(ArtefactStatus newStatus) async {
20+
final dio = ref.watch(dioProvider);
21+
22+
final response = await dio.patch(
23+
'/v1/artefacts/$artefactId',
24+
data: {'status': newStatus.toJson()},
25+
);
26+
27+
state = AsyncData(Artefact.fromJson(response.data));
28+
}
29+
}

frontend/lib/routing.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ final appRouter = GoRouter(
2828
path: ':artefactId',
2929
pageBuilder: (context, state) => DialogPage(
3030
builder: (_) => ArtefactDialog(
31-
artefactId: state.pathParameters['artefactId']!,
31+
artefactId: int.parse(state.pathParameters['artefactId']!),
3232
),
3333
),
3434
),
@@ -44,7 +44,7 @@ final appRouter = GoRouter(
4444
path: ':artefactId',
4545
pageBuilder: (context, state) => DialogPage(
4646
builder: (_) => ArtefactDialog(
47-
artefactId: state.pathParameters['artefactId']!,
47+
artefactId: int.parse(state.pathParameters['artefactId']!),
4848
),
4949
),
5050
),

frontend/lib/ui/artefact_dialog/artefact_dialog.dart

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,20 @@ import 'package:flutter/material.dart';
44
import 'package:flutter_riverpod/flutter_riverpod.dart';
55
import 'package:yaru_widgets/widgets.dart';
66

7-
import '../../providers/artefact.dart';
7+
import '../../providers/artefact_notifier.dart';
88
import '../spacing.dart';
99
import 'artefact_dialog_body.dart';
10-
import 'artefact_dialog_headert.dart';
10+
import 'artefact_dialog_header.dart';
1111
import 'artefact_dialog_info_section.dart';
1212

1313
class ArtefactDialog extends ConsumerWidget {
1414
const ArtefactDialog({super.key, required this.artefactId});
1515

16-
final String artefactId;
16+
final int artefactId;
1717

1818
@override
1919
Widget build(BuildContext context, WidgetRef ref) {
20-
final artefact = ref.watch(artefactProvider(artefactId));
20+
final artefact = ref.watch(artefactNotifierProvider(artefactId));
2121

2222
return SelectionArea(
2323
child: Dialog(
@@ -33,7 +33,7 @@ class ArtefactDialog extends ConsumerWidget {
3333
data: (artefact) => Column(
3434
crossAxisAlignment: CrossAxisAlignment.start,
3535
children: [
36-
ArtefactDialogHeader(title: artefact.name),
36+
ArtefactDialogHeader(artefact: artefact),
3737
const SizedBox(height: Spacing.level4),
3838
ArtefactDialogInfoSection(artefact: artefact),
3939
const SizedBox(height: Spacing.level4),

frontend/lib/ui/artefact_dialog/artefact_dialog_headert.dart renamed to frontend/lib/ui/artefact_dialog/artefact_dialog_header.dart

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import 'package:flutter/material.dart';
22
import 'package:yaru_icons/yaru_icons.dart';
33

4+
import '../../models/artefact.dart';
45
import '../spacing.dart';
6+
import 'artefact_signoff_button.dart';
57

68
class ArtefactDialogHeader extends StatelessWidget {
7-
const ArtefactDialogHeader({super.key, required this.title});
9+
const ArtefactDialogHeader({super.key, required this.artefact});
810

9-
final String title;
11+
final Artefact artefact;
1012

1113
@override
1214
Widget build(BuildContext context) {
@@ -18,9 +20,11 @@ class ArtefactDialogHeader extends StatelessWidget {
1820
),
1921
),
2022
child: Row(
21-
mainAxisAlignment: MainAxisAlignment.spaceBetween,
2223
children: [
23-
Text(title, style: Theme.of(context).textTheme.headlineLarge),
24+
Text(artefact.name, style: Theme.of(context).textTheme.headlineLarge),
25+
const SizedBox(width: Spacing.level4),
26+
ArtefactSignoffButton(artefact: artefact),
27+
const Spacer(),
2428
InkWell(
2529
child: const Icon(
2630
YaruIcons.window_close,
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_riverpod/flutter_riverpod.dart';
3+
import 'package:yaru_widgets/yaru_widgets.dart';
4+
5+
import '../../models/artefact.dart';
6+
import '../../providers/artefact_notifier.dart';
7+
8+
class ArtefactSignoffButton extends ConsumerWidget {
9+
const ArtefactSignoffButton({super.key, required this.artefact});
10+
11+
final Artefact artefact;
12+
13+
@override
14+
Widget build(BuildContext context, WidgetRef ref) {
15+
final fontStyle = Theme.of(context).textTheme.titleMedium;
16+
17+
return YaruPopupMenuButton(
18+
child: Text(
19+
artefact.status.name,
20+
style: fontStyle?.apply(color: artefact.status.color),
21+
),
22+
itemBuilder: (_) => ArtefactStatus.values
23+
.map(
24+
(status) => PopupMenuItem(
25+
value: status,
26+
onTap: () => ref
27+
.read(artefactNotifierProvider(artefact.id).notifier)
28+
.changeStatus(status),
29+
child: Text(
30+
status.name,
31+
style: fontStyle?.apply(color: status.color),
32+
),
33+
),
34+
)
35+
.toList(),
36+
);
37+
}
38+
}

0 commit comments

Comments
 (0)