goosebit 0.2.3__py3-none-any.whl → 0.2.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- goosebit/__init__.py +16 -3
- goosebit/api/v1/devices/device/routes.py +8 -2
- goosebit/api/v1/devices/responses.py +0 -7
- goosebit/api/v1/devices/routes.py +2 -1
- goosebit/api/v1/rollouts/responses.py +2 -7
- goosebit/api/v1/rollouts/routes.py +7 -3
- goosebit/api/v1/software/responses.py +0 -7
- goosebit/api/v1/software/routes.py +24 -11
- goosebit/auth/__init__.py +7 -7
- goosebit/db/__init__.py +12 -1
- goosebit/db/models.py +13 -2
- goosebit/schema/devices.py +41 -37
- goosebit/schema/rollouts.py +21 -18
- goosebit/schema/software.py +24 -19
- goosebit/ui/bff/common/__init__.py +0 -0
- goosebit/ui/bff/common/requests.py +56 -0
- goosebit/ui/bff/common/util.py +32 -0
- goosebit/ui/bff/devices/responses.py +12 -20
- goosebit/ui/bff/devices/routes.py +9 -6
- goosebit/ui/bff/rollouts/responses.py +12 -20
- goosebit/ui/bff/rollouts/routes.py +8 -6
- goosebit/ui/bff/software/responses.py +19 -19
- goosebit/ui/bff/software/routes.py +29 -16
- goosebit/ui/nav.py +1 -1
- goosebit/ui/routes.py +4 -4
- goosebit/ui/static/js/devices.js +135 -25
- goosebit/ui/static/js/rollouts.js +4 -0
- goosebit/ui/static/js/util.js +23 -14
- goosebit/ui/templates/devices.html.jinja +77 -29
- goosebit/ui/templates/nav.html.jinja +22 -2
- goosebit/ui/templates/rollouts.html.jinja +23 -23
- goosebit/updater/controller/v1/routes.py +7 -3
- goosebit/updater/controller/v1/schema.py +4 -4
- goosebit/updater/manager.py +16 -8
- goosebit/updates/__init__.py +14 -21
- goosebit/updates/swdesc.py +35 -14
- {goosebit-0.2.3.dist-info → goosebit-0.2.4.dist-info}/METADATA +11 -3
- {goosebit-0.2.3.dist-info → goosebit-0.2.4.dist-info}/RECORD +40 -37
- {goosebit-0.2.3.dist-info → goosebit-0.2.4.dist-info}/LICENSE +0 -0
- {goosebit-0.2.3.dist-info → goosebit-0.2.4.dist-info}/WHEEL +0 -0
@@ -1,11 +1,12 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
import
|
3
|
+
from typing import Callable
|
4
4
|
|
5
|
-
from fastapi.requests import Request
|
6
5
|
from pydantic import BaseModel, Field
|
6
|
+
from tortoise.queryset import QuerySet
|
7
7
|
|
8
8
|
from goosebit.schema.devices import DeviceSchema
|
9
|
+
from goosebit.ui.bff.common.requests import DataTableRequest
|
9
10
|
|
10
11
|
|
11
12
|
class BFFDeviceResponse(BaseModel):
|
@@ -15,25 +16,16 @@ class BFFDeviceResponse(BaseModel):
|
|
15
16
|
records_filtered: int = Field(serialization_alias="recordsFiltered")
|
16
17
|
|
17
18
|
@classmethod
|
18
|
-
async def convert(cls,
|
19
|
-
|
19
|
+
async def convert(cls, dt_query: DataTableRequest, query: QuerySet, search_filter: Callable):
|
20
|
+
total_records = await query.count()
|
21
|
+
if dt_query.search.value:
|
22
|
+
query = query.filter(search_filter(dt_query.search.value))
|
20
23
|
|
21
|
-
|
22
|
-
|
23
|
-
length = int(params.get("length", 10))
|
24
|
-
search_value = params.get("search[value]", None)
|
25
|
-
order_column_index = params.get("order[0][column]", None)
|
26
|
-
order_column = params.get(f"columns[{order_column_index}][data]", None)
|
27
|
-
order_dir = params.get("order[0][dir]", None)
|
28
|
-
|
29
|
-
if search_value:
|
30
|
-
query = query.filter(search_filter(search_value))
|
31
|
-
|
32
|
-
if order_column:
|
33
|
-
query = query.order_by(f"{'-' if order_dir == 'desc' else ''}{order_column}")
|
24
|
+
if dt_query.order_query:
|
25
|
+
query = query.order_by(dt_query.order_query)
|
34
26
|
|
35
27
|
filtered_records = await query.count()
|
36
|
-
devices = await query.offset(start).limit(length).all()
|
37
|
-
data =
|
28
|
+
devices = await query.offset(dt_query.start).limit(dt_query.length).all()
|
29
|
+
data = [DeviceSchema.model_validate(d) for d in devices]
|
38
30
|
|
39
|
-
return cls(data=data, draw=draw, records_total=total_records, records_filtered=filtered_records)
|
31
|
+
return cls(data=data, draw=dt_query.draw, records_total=total_records, records_filtered=filtered_records)
|
@@ -1,6 +1,8 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
from
|
3
|
+
from typing import Annotated
|
4
|
+
|
5
|
+
from fastapi import APIRouter, Depends, Security
|
4
6
|
from fastapi.requests import Request
|
5
7
|
from tortoise.expressions import Q
|
6
8
|
|
@@ -8,6 +10,8 @@ from goosebit.api.responses import StatusResponse
|
|
8
10
|
from goosebit.api.v1.devices import routes
|
9
11
|
from goosebit.auth import validate_user_permissions
|
10
12
|
from goosebit.db.models import Device, Software, UpdateModeEnum, UpdateStateEnum
|
13
|
+
from goosebit.ui.bff.common.requests import DataTableRequest
|
14
|
+
from goosebit.ui.bff.common.util import parse_datatables_query
|
11
15
|
from goosebit.updater.manager import get_update_manager
|
12
16
|
|
13
17
|
from .requests import DevicesPatchRequest
|
@@ -20,8 +24,8 @@ router = APIRouter(prefix="/devices")
|
|
20
24
|
"",
|
21
25
|
dependencies=[Security(validate_user_permissions, scopes=["home.read"])],
|
22
26
|
)
|
23
|
-
async def devices_get(
|
24
|
-
def search_filter(search_value):
|
27
|
+
async def devices_get(dt_query: Annotated[DataTableRequest, Depends(parse_datatables_query)]) -> BFFDeviceResponse:
|
28
|
+
def search_filter(search_value: str):
|
25
29
|
return (
|
26
30
|
Q(uuid__icontains=search_value)
|
27
31
|
| Q(name__icontains=search_value)
|
@@ -31,10 +35,9 @@ async def devices_get(request: Request) -> BFFDeviceResponse:
|
|
31
35
|
| Q(last_state=int(UpdateStateEnum.from_str(search_value)))
|
32
36
|
)
|
33
37
|
|
34
|
-
query = Device.all().prefetch_related("assigned_software", "hardware")
|
35
|
-
total_records = await Device.all().count()
|
38
|
+
query = Device.all().prefetch_related("assigned_software", "hardware", "assigned_software__compatibility")
|
36
39
|
|
37
|
-
return await BFFDeviceResponse.convert(
|
40
|
+
return await BFFDeviceResponse.convert(dt_query, query, search_filter)
|
38
41
|
|
39
42
|
|
40
43
|
@router.patch(
|
@@ -1,9 +1,10 @@
|
|
1
|
-
import
|
1
|
+
from typing import Callable
|
2
2
|
|
3
|
-
from fastapi.requests import Request
|
4
3
|
from pydantic import BaseModel, Field
|
4
|
+
from tortoise.queryset import QuerySet
|
5
5
|
|
6
6
|
from goosebit.schema.rollouts import RolloutSchema
|
7
|
+
from goosebit.ui.bff.common.requests import DataTableRequest
|
7
8
|
|
8
9
|
|
9
10
|
class BFFRolloutsResponse(BaseModel):
|
@@ -13,25 +14,16 @@ class BFFRolloutsResponse(BaseModel):
|
|
13
14
|
records_filtered: int = Field(serialization_alias="recordsFiltered")
|
14
15
|
|
15
16
|
@classmethod
|
16
|
-
async def convert(cls,
|
17
|
-
|
17
|
+
async def convert(cls, dt_query: DataTableRequest, query: QuerySet, search_filter: Callable):
|
18
|
+
total_records = await query.count()
|
19
|
+
if dt_query.search.value:
|
20
|
+
query = query.filter(search_filter(dt_query.search.value))
|
18
21
|
|
19
|
-
|
20
|
-
|
21
|
-
length = int(params.get("length", 10))
|
22
|
-
search_value = params.get("search[value]", None)
|
23
|
-
order_column_index = params.get("order[0][column]", None)
|
24
|
-
order_column = params.get(f"columns[{order_column_index}][data]", None)
|
25
|
-
order_dir = params.get("order[0][dir]", None)
|
26
|
-
|
27
|
-
if search_value:
|
28
|
-
query = query.filter(search_filter(search_value))
|
29
|
-
|
30
|
-
if order_column:
|
31
|
-
query = query.order_by(f"{'-' if order_dir == 'desc' else ''}{order_column}")
|
22
|
+
if dt_query.order_query:
|
23
|
+
query = query.order_by(dt_query.order_query)
|
32
24
|
|
33
25
|
filtered_records = await query.count()
|
34
|
-
rollouts = await query.offset(start).limit(length).all()
|
35
|
-
data =
|
26
|
+
rollouts = await query.offset(dt_query.start).limit(dt_query.length).all()
|
27
|
+
data = [RolloutSchema.model_validate(r) for r in rollouts]
|
36
28
|
|
37
|
-
return cls(data=data, draw=draw, records_total=total_records, records_filtered=filtered_records)
|
29
|
+
return cls(data=data, draw=dt_query.draw, records_total=total_records, records_filtered=filtered_records)
|
@@ -1,10 +1,13 @@
|
|
1
|
-
from
|
2
|
-
|
1
|
+
from typing import Annotated
|
2
|
+
|
3
|
+
from fastapi import APIRouter, Depends, Security
|
3
4
|
from tortoise.expressions import Q
|
4
5
|
|
5
6
|
from goosebit.api.v1.rollouts import routes
|
6
7
|
from goosebit.auth import validate_user_permissions
|
7
8
|
from goosebit.db.models import Rollout
|
9
|
+
from goosebit.ui.bff.common.requests import DataTableRequest
|
10
|
+
from goosebit.ui.bff.common.util import parse_datatables_query
|
8
11
|
|
9
12
|
from .responses import BFFRolloutsResponse
|
10
13
|
|
@@ -15,14 +18,13 @@ router = APIRouter(prefix="/rollouts")
|
|
15
18
|
"",
|
16
19
|
dependencies=[Security(validate_user_permissions, scopes=["rollout.read"])],
|
17
20
|
)
|
18
|
-
async def rollouts_get(
|
21
|
+
async def rollouts_get(dt_query: Annotated[DataTableRequest, Depends(parse_datatables_query)]) -> BFFRolloutsResponse:
|
19
22
|
def search_filter(search_value):
|
20
23
|
return Q(name__icontains=search_value) | Q(feed__icontains=search_value)
|
21
24
|
|
22
|
-
query = Rollout.all().prefetch_related("software")
|
23
|
-
total_records = await Rollout.all().count()
|
25
|
+
query = Rollout.all().prefetch_related("software", "software__compatibility")
|
24
26
|
|
25
|
-
return await BFFRolloutsResponse.convert(
|
27
|
+
return await BFFRolloutsResponse.convert(dt_query, query, search_filter)
|
26
28
|
|
27
29
|
|
28
30
|
router.add_api_route(
|
@@ -1,9 +1,11 @@
|
|
1
|
-
import
|
1
|
+
from typing import Callable
|
2
2
|
|
3
|
-
from fastapi.requests import Request
|
4
3
|
from pydantic import BaseModel, Field
|
4
|
+
from tortoise.expressions import Q
|
5
|
+
from tortoise.queryset import QuerySet
|
5
6
|
|
6
7
|
from goosebit.schema.software import SoftwareSchema
|
8
|
+
from goosebit.ui.bff.common.requests import DataTableRequest
|
7
9
|
|
8
10
|
|
9
11
|
class BFFSoftwareResponse(BaseModel):
|
@@ -13,25 +15,23 @@ class BFFSoftwareResponse(BaseModel):
|
|
13
15
|
records_filtered: int = Field(serialization_alias="recordsFiltered")
|
14
16
|
|
15
17
|
@classmethod
|
16
|
-
async def convert(cls,
|
17
|
-
|
18
|
+
async def convert(cls, dt_query: DataTableRequest, query: QuerySet, search_filter: Callable, alt_filter: Q):
|
19
|
+
total_records = await query.count()
|
20
|
+
query = query.filter(alt_filter)
|
21
|
+
if dt_query.search.value:
|
22
|
+
query = query.filter(search_filter(dt_query.search.value))
|
18
23
|
|
19
|
-
|
20
|
-
|
21
|
-
length = int(params.get("length", 10))
|
22
|
-
search_value = params.get("search[value]", None)
|
23
|
-
order_column_index = params.get("order[0][column]", None)
|
24
|
-
order_column = params.get(f"columns[{order_column_index}][data]", None)
|
25
|
-
order_dir = params.get("order[0][dir]", None)
|
24
|
+
if dt_query.order_query:
|
25
|
+
query = query.order_by(dt_query.order_query)
|
26
26
|
|
27
|
-
|
28
|
-
query = query.filter(search_filter(search_value))
|
27
|
+
filtered_records = await query.count()
|
29
28
|
|
30
|
-
|
31
|
-
query = query.order_by(f"{'-' if order_dir == 'desc' else ''}{order_column}")
|
29
|
+
query = query.offset(dt_query.start)
|
32
30
|
|
33
|
-
|
34
|
-
|
35
|
-
|
31
|
+
if not dt_query.length == 0:
|
32
|
+
query = query.limit(dt_query.length)
|
33
|
+
|
34
|
+
devices = await query.all()
|
35
|
+
data = [SoftwareSchema.model_validate(d) for d in devices]
|
36
36
|
|
37
|
-
return cls(data=data, draw=draw, records_total=total_records, records_filtered=filtered_records)
|
37
|
+
return cls(data=data, draw=dt_query.draw, records_total=total_records, records_filtered=filtered_records)
|
@@ -1,14 +1,18 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
import
|
4
|
-
|
3
|
+
from typing import Annotated
|
4
|
+
|
5
|
+
from anyio import Path, open_file
|
6
|
+
from fastapi import APIRouter, Depends, Form, HTTPException, Query, Security, UploadFile
|
5
7
|
from fastapi.requests import Request
|
6
8
|
from tortoise.expressions import Q
|
7
9
|
|
8
10
|
from goosebit.api.v1.software import routes
|
9
11
|
from goosebit.auth import validate_user_permissions
|
10
|
-
from goosebit.db.models import Rollout, Software
|
12
|
+
from goosebit.db.models import Hardware, Rollout, Software
|
11
13
|
from goosebit.settings import config
|
14
|
+
from goosebit.ui.bff.common.requests import DataTableRequest
|
15
|
+
from goosebit.ui.bff.common.util import parse_datatables_query
|
12
16
|
from goosebit.updates import create_software_update
|
13
17
|
|
14
18
|
from .responses import BFFSoftwareResponse
|
@@ -20,14 +24,23 @@ router = APIRouter(prefix="/software")
|
|
20
24
|
"",
|
21
25
|
dependencies=[Security(validate_user_permissions, scopes=["software.read"])],
|
22
26
|
)
|
23
|
-
async def software_get(
|
27
|
+
async def software_get(
|
28
|
+
dt_query: Annotated[DataTableRequest, Depends(parse_datatables_query)],
|
29
|
+
uuids: list[str] = Query(default=None),
|
30
|
+
) -> BFFSoftwareResponse:
|
31
|
+
filters: list[Q] = []
|
32
|
+
|
24
33
|
def search_filter(search_value):
|
25
|
-
|
34
|
+
base_filter = Q(Q(uri__icontains=search_value), Q(version__icontains=search_value), join_type="OR")
|
35
|
+
return Q(base_filter, *filters, join_type="AND")
|
26
36
|
|
27
37
|
query = Software.all().prefetch_related("compatibility")
|
28
|
-
total_records = await Software.all().count()
|
29
38
|
|
30
|
-
|
39
|
+
if uuids:
|
40
|
+
hardware = await Hardware.filter(devices__uuid__in=uuids).distinct()
|
41
|
+
filters.append(Q(*[Q(compatibility__id=c.id) for c in hardware], join_type="AND"))
|
42
|
+
|
43
|
+
return await BFFSoftwareResponse.convert(dt_query, query, search_filter, Q(*filters))
|
31
44
|
|
32
45
|
|
33
46
|
router.add_api_route(
|
@@ -64,20 +77,20 @@ async def post_update(
|
|
64
77
|
await create_software_update(url, None)
|
65
78
|
else:
|
66
79
|
# local file
|
67
|
-
|
68
|
-
|
80
|
+
artifacts_dir = Path(config.artifacts_dir)
|
81
|
+
file = artifacts_dir.joinpath(filename)
|
82
|
+
await artifacts_dir.mkdir(parents=True, exist_ok=True)
|
69
83
|
|
70
84
|
temp_file = file.with_suffix(".tmp")
|
71
85
|
if init:
|
72
|
-
temp_file.unlink(missing_ok=True)
|
73
|
-
|
74
|
-
contents = await chunk.read()
|
86
|
+
await temp_file.unlink(missing_ok=True)
|
75
87
|
|
76
|
-
async with
|
77
|
-
await f.write(
|
88
|
+
async with await open_file(temp_file, "ab") as f:
|
89
|
+
await f.write(await chunk.read())
|
78
90
|
|
79
91
|
if done:
|
80
92
|
try:
|
81
|
-
await
|
93
|
+
absolute = await file.absolute()
|
94
|
+
await create_software_update(absolute.as_uri(), temp_file)
|
82
95
|
finally:
|
83
|
-
temp_file.unlink(missing_ok=True)
|
96
|
+
await temp_file.unlink(missing_ok=True)
|
goosebit/ui/nav.py
CHANGED
@@ -2,7 +2,7 @@ class Navigation:
|
|
2
2
|
def __init__(self):
|
3
3
|
self.items = []
|
4
4
|
|
5
|
-
def route(self, text: str, permissions: str = None):
|
5
|
+
def route(self, text: str, permissions: str | None = None):
|
6
6
|
def decorator(func):
|
7
7
|
self.items.append({"function": func.__name__, "text": text, "permissions": permissions})
|
8
8
|
return func
|
goosebit/ui/routes.py
CHANGED
@@ -24,7 +24,7 @@ async def ui_root(request: Request):
|
|
24
24
|
"/home",
|
25
25
|
dependencies=[Security(validate_user_permissions, scopes=["home.read"])],
|
26
26
|
)
|
27
|
-
@nav.route("Home", permissions=
|
27
|
+
@nav.route("Home", permissions="home.read")
|
28
28
|
async def home_ui(request: Request):
|
29
29
|
return templates.TemplateResponse(request, "index.html.jinja", context={"title": "Home"})
|
30
30
|
|
@@ -33,7 +33,7 @@ async def home_ui(request: Request):
|
|
33
33
|
"/devices",
|
34
34
|
dependencies=[Security(validate_user_permissions, scopes=["device.read"])],
|
35
35
|
)
|
36
|
-
@nav.route("Devices", permissions=
|
36
|
+
@nav.route("Devices", permissions="device.read")
|
37
37
|
async def devices_ui(request: Request):
|
38
38
|
return templates.TemplateResponse(request, "devices.html.jinja", context={"title": "Devices"})
|
39
39
|
|
@@ -42,7 +42,7 @@ async def devices_ui(request: Request):
|
|
42
42
|
"/software",
|
43
43
|
dependencies=[Security(validate_user_permissions, scopes=["software.read"])],
|
44
44
|
)
|
45
|
-
@nav.route("Software", permissions=
|
45
|
+
@nav.route("Software", permissions="software.read")
|
46
46
|
async def software_ui(request: Request):
|
47
47
|
return templates.TemplateResponse(request, "software.html.jinja", context={"title": "Software"})
|
48
48
|
|
@@ -51,7 +51,7 @@ async def software_ui(request: Request):
|
|
51
51
|
"/rollouts",
|
52
52
|
dependencies=[Security(validate_user_permissions, scopes=["rollout.read"])],
|
53
53
|
)
|
54
|
-
@nav.route("Rollouts", permissions=
|
54
|
+
@nav.route("Rollouts", permissions="rollout.read")
|
55
55
|
async def rollouts_ui(request: Request):
|
56
56
|
return templates.TemplateResponse(request, "rollouts.html.jinja", context={"title": "Rollouts"})
|
57
57
|
|
goosebit/ui/static/js/devices.js
CHANGED
@@ -124,20 +124,11 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
124
124
|
{
|
125
125
|
text: '<i class="bi bi-pen" ></i>',
|
126
126
|
action: () => {
|
127
|
-
const
|
128
|
-
|
127
|
+
const selectedDevices = dataTable.rows({ selected: true }).data().toArray();
|
128
|
+
const selectedDevice = selectedDevices[0];
|
129
|
+
updateSoftwareSelection(selectedDevices);
|
130
|
+
$("#device-name").val(selectedDevice.name);
|
129
131
|
$("#device-selected-feed").val(selectedDevice.feed);
|
130
|
-
|
131
|
-
let selectedValue;
|
132
|
-
if (selectedDevice.update_mode === "Rollout") {
|
133
|
-
selectedValue = "rollout";
|
134
|
-
} else if (selectedDevice.update_mode === "Latest") {
|
135
|
-
selectedValue = "latest";
|
136
|
-
} else {
|
137
|
-
selectedValue = selectedDevice.sw_assigned;
|
138
|
-
}
|
139
|
-
$("#selected-sw").val(selectedValue);
|
140
|
-
|
141
132
|
new bootstrap.Modal("#device-config-modal").show();
|
142
133
|
},
|
143
134
|
className: "buttons-config",
|
@@ -199,22 +190,89 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
199
190
|
dataTable.ajax.reload(null, false);
|
200
191
|
}, TABLE_UPDATE_TIME);
|
201
192
|
|
202
|
-
await updateSoftwareSelection(
|
193
|
+
await updateSoftwareSelection();
|
194
|
+
|
195
|
+
// Name update form submit
|
196
|
+
const nameForm = document.getElementById("device-name-form");
|
197
|
+
nameForm.addEventListener(
|
198
|
+
"submit",
|
199
|
+
async (event) => {
|
200
|
+
if (nameForm.checkValidity() === false) {
|
201
|
+
event.preventDefault();
|
202
|
+
event.stopPropagation();
|
203
|
+
nameForm.classList.add("was-validated");
|
204
|
+
} else {
|
205
|
+
event.preventDefault();
|
206
|
+
await updateDeviceName();
|
207
|
+
nameForm.classList.remove("was-validated");
|
208
|
+
nameForm.reset();
|
209
|
+
const modal = bootstrap.Modal.getInstance(document.getElementById("device-config-modal"));
|
210
|
+
modal.hide();
|
211
|
+
}
|
212
|
+
},
|
213
|
+
false,
|
214
|
+
);
|
215
|
+
|
216
|
+
// Rollout form submit
|
217
|
+
const rolloutForm = document.getElementById("device-software-rollout-form");
|
218
|
+
rolloutForm.addEventListener(
|
219
|
+
"submit",
|
220
|
+
async (event) => {
|
221
|
+
if (rolloutForm.checkValidity() === false) {
|
222
|
+
event.preventDefault();
|
223
|
+
event.stopPropagation();
|
224
|
+
rolloutForm.classList.add("was-validated");
|
225
|
+
} else {
|
226
|
+
event.preventDefault();
|
227
|
+
await updateDeviceRollout();
|
228
|
+
rolloutForm.classList.remove("was-validated");
|
229
|
+
rolloutForm.reset();
|
230
|
+
const modal = bootstrap.Modal.getInstance(document.getElementById("device-config-modal"));
|
231
|
+
modal.hide();
|
232
|
+
}
|
233
|
+
},
|
234
|
+
false,
|
235
|
+
);
|
236
|
+
|
237
|
+
// Manual software form submit
|
238
|
+
const manualSoftwareForm = document.getElementById("device-software-manual-form");
|
239
|
+
manualSoftwareForm.addEventListener(
|
240
|
+
"submit",
|
241
|
+
async (event) => {
|
242
|
+
if (manualSoftwareForm.checkValidity() === false) {
|
243
|
+
event.preventDefault();
|
244
|
+
event.stopPropagation();
|
245
|
+
manualSoftwareForm.classList.add("was-validated");
|
246
|
+
if (document.getElementById("selected-sw").value === "") {
|
247
|
+
document.getElementById("selected-sw").parentElement.classList.add("is-invalid");
|
248
|
+
}
|
249
|
+
} else {
|
250
|
+
event.preventDefault();
|
251
|
+
await updateDeviceManualSoftware();
|
252
|
+
manualSoftwareForm.classList.remove("was-validated");
|
253
|
+
document.getElementById("selected-sw").parentElement.classList.remove("is-invalid");
|
254
|
+
manualSoftwareForm.reset();
|
255
|
+
const modal = bootstrap.Modal.getInstance(document.getElementById("device-config-modal"));
|
256
|
+
modal.hide();
|
257
|
+
}
|
258
|
+
},
|
259
|
+
false,
|
260
|
+
);
|
203
261
|
|
204
|
-
//
|
205
|
-
const
|
206
|
-
|
262
|
+
// Latest software form submit
|
263
|
+
const latestSoftwareForm = document.getElementById("device-software-latest-form");
|
264
|
+
latestSoftwareForm.addEventListener(
|
207
265
|
"submit",
|
208
266
|
async (event) => {
|
209
|
-
if (
|
267
|
+
if (latestSoftwareForm.checkValidity() === false) {
|
210
268
|
event.preventDefault();
|
211
269
|
event.stopPropagation();
|
212
|
-
|
270
|
+
latestSoftwareForm.classList.add("was-validated");
|
213
271
|
} else {
|
214
272
|
event.preventDefault();
|
215
|
-
await
|
216
|
-
|
217
|
-
|
273
|
+
await updateDeviceLatest();
|
274
|
+
latestSoftwareForm.classList.remove("was-validated");
|
275
|
+
latestSoftwareForm.reset();
|
218
276
|
const modal = bootstrap.Modal.getInstance(document.getElementById("device-config-modal"));
|
219
277
|
modal.hide();
|
220
278
|
}
|
@@ -244,18 +302,70 @@ function updateBtnState() {
|
|
244
302
|
}
|
245
303
|
}
|
246
304
|
|
247
|
-
async function
|
305
|
+
async function updateDeviceName() {
|
306
|
+
const devices = dataTable
|
307
|
+
.rows({ selected: true })
|
308
|
+
.data()
|
309
|
+
.toArray()
|
310
|
+
.map((d) => d.uuid);
|
311
|
+
const name = document.getElementById("device-name").value;
|
312
|
+
|
313
|
+
try {
|
314
|
+
await patch_request("/ui/bff/devices", { devices, name });
|
315
|
+
} catch (error) {
|
316
|
+
console.error("Update device config failed:", error);
|
317
|
+
}
|
318
|
+
|
319
|
+
setTimeout(updateDeviceList, 50);
|
320
|
+
}
|
321
|
+
|
322
|
+
async function updateDeviceRollout() {
|
248
323
|
const devices = dataTable
|
249
324
|
.rows({ selected: true })
|
250
325
|
.data()
|
251
326
|
.toArray()
|
252
327
|
.map((d) => d.uuid);
|
253
|
-
const name = document.getElementById("device-selected-name").value;
|
254
328
|
const feed = document.getElementById("device-selected-feed").value;
|
329
|
+
const software = "rollout";
|
330
|
+
|
331
|
+
try {
|
332
|
+
await patch_request("/ui/bff/devices", { devices, feed, software });
|
333
|
+
} catch (error) {
|
334
|
+
console.error("Update device config failed:", error);
|
335
|
+
}
|
336
|
+
|
337
|
+
setTimeout(updateDeviceList, 50);
|
338
|
+
}
|
339
|
+
|
340
|
+
async function updateDeviceManualSoftware() {
|
341
|
+
const devices = dataTable
|
342
|
+
.rows({ selected: true })
|
343
|
+
.data()
|
344
|
+
.toArray()
|
345
|
+
.map((d) => d.uuid);
|
346
|
+
const feed = null;
|
255
347
|
const software = document.getElementById("selected-sw").value;
|
256
348
|
|
257
349
|
try {
|
258
|
-
await patch_request("/ui/bff/devices", { devices,
|
350
|
+
await patch_request("/ui/bff/devices", { devices, feed, software });
|
351
|
+
} catch (error) {
|
352
|
+
console.error("Update device config failed:", error);
|
353
|
+
}
|
354
|
+
|
355
|
+
setTimeout(updateDeviceList, 50);
|
356
|
+
}
|
357
|
+
|
358
|
+
async function updateDeviceLatest() {
|
359
|
+
const devices = dataTable
|
360
|
+
.rows({ selected: true })
|
361
|
+
.data()
|
362
|
+
.toArray()
|
363
|
+
.map((d) => d.uuid);
|
364
|
+
const feed = null;
|
365
|
+
const software = "latest";
|
366
|
+
|
367
|
+
try {
|
368
|
+
await patch_request("/ui/bff/devices", { devices, feed, software });
|
259
369
|
} catch (error) {
|
260
370
|
console.error("Update device config failed:", error);
|
261
371
|
}
|
@@ -136,6 +136,9 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
136
136
|
"submit",
|
137
137
|
(event) => {
|
138
138
|
if (form.checkValidity() === false) {
|
139
|
+
if (document.getElementById("selected-sw").value === "") {
|
140
|
+
document.getElementById("selected-sw").parentElement.classList.add("is-invalid");
|
141
|
+
}
|
139
142
|
event.preventDefault();
|
140
143
|
event.stopPropagation();
|
141
144
|
form.classList.add("was-validated");
|
@@ -143,6 +146,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
143
146
|
event.preventDefault();
|
144
147
|
createRollout();
|
145
148
|
form.classList.remove("was-validated");
|
149
|
+
document.getElementById("selected-sw").parentElement.classList.remove("is-invalid");
|
146
150
|
form.reset();
|
147
151
|
const modal = bootstrap.Modal.getInstance(document.getElementById("rollout-create-modal"));
|
148
152
|
modal.hide();
|
goosebit/ui/static/js/util.js
CHANGED
@@ -20,27 +20,22 @@ function secondsToRecentDate(t) {
|
|
20
20
|
return s + (s === 1 ? " second" : " seconds");
|
21
21
|
}
|
22
22
|
|
23
|
-
async function updateSoftwareSelection(
|
23
|
+
async function updateSoftwareSelection(devices = null) {
|
24
24
|
try {
|
25
|
-
const
|
25
|
+
const url = new URL("/ui/bff/software", window.location.origin);
|
26
|
+
if (devices != null) {
|
27
|
+
for (const device of devices) {
|
28
|
+
url.searchParams.append("uuids", device.uuid);
|
29
|
+
}
|
30
|
+
}
|
31
|
+
const response = await fetch(url.toString());
|
26
32
|
if (!response.ok) {
|
27
33
|
console.error("Retrieving software list failed.");
|
28
34
|
return;
|
29
35
|
}
|
30
36
|
const data = (await response.json()).data;
|
31
37
|
const selectElem = document.getElementById("selected-sw");
|
32
|
-
|
33
|
-
if (addSpecialMode) {
|
34
|
-
let optionElem = document.createElement("option");
|
35
|
-
optionElem.value = "rollout";
|
36
|
-
optionElem.textContent = "Rollout";
|
37
|
-
selectElem.appendChild(optionElem);
|
38
|
-
|
39
|
-
optionElem = document.createElement("option");
|
40
|
-
optionElem.value = "latest";
|
41
|
-
optionElem.textContent = "Latest";
|
42
|
-
selectElem.appendChild(optionElem);
|
43
|
-
}
|
38
|
+
selectElem.innerHTML = "";
|
44
39
|
|
45
40
|
for (const item of data) {
|
46
41
|
const optionElem = document.createElement("option");
|
@@ -50,6 +45,20 @@ async function updateSoftwareSelection(addSpecialMode = false) {
|
|
50
45
|
optionElem.textContent = `${item.version} (${models})`;
|
51
46
|
selectElem.appendChild(optionElem);
|
52
47
|
}
|
48
|
+
$("#selected-sw").selectpicker("destroy");
|
49
|
+
if (data.length === 0) {
|
50
|
+
selectElem.title = "No valid software found for selected device";
|
51
|
+
if (devices != null) {
|
52
|
+
if (devices.length > 1) {
|
53
|
+
selectElem.title += "s";
|
54
|
+
}
|
55
|
+
}
|
56
|
+
selectElem.disabled = true;
|
57
|
+
} else {
|
58
|
+
selectElem.disabled = false;
|
59
|
+
selectElem.title = "Select Software";
|
60
|
+
}
|
61
|
+
$("#selected-sw").selectpicker();
|
53
62
|
} catch (error) {
|
54
63
|
console.error("Failed to fetch device data:", error);
|
55
64
|
}
|