goosebit 0.2.5__py3-none-any.whl → 0.2.7__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 +41 -7
- goosebit/api/telemetry/metrics.py +1 -5
- goosebit/api/v1/devices/device/responses.py +1 -0
- goosebit/api/v1/devices/device/routes.py +8 -8
- goosebit/api/v1/devices/requests.py +20 -0
- goosebit/api/v1/devices/routes.py +68 -8
- goosebit/api/v1/download/routes.py +14 -3
- goosebit/api/v1/rollouts/routes.py +5 -4
- goosebit/api/v1/routes.py +2 -1
- goosebit/api/v1/settings/routes.py +14 -0
- goosebit/api/v1/settings/users/__init__.py +1 -0
- goosebit/api/v1/settings/users/requests.py +16 -0
- goosebit/api/v1/settings/users/responses.py +7 -0
- goosebit/api/v1/settings/users/routes.py +56 -0
- goosebit/api/v1/software/routes.py +18 -14
- goosebit/auth/__init__.py +49 -13
- goosebit/auth/permissions.py +80 -0
- goosebit/db/config.py +57 -1
- goosebit/db/migrations/models/2_20241121113728_update.py +11 -0
- goosebit/db/migrations/models/3_20241121140210_update.py +11 -0
- goosebit/db/migrations/models/4_20250324110331_update.py +16 -0
- goosebit/db/migrations/models/4_20250402085235_rename_uuid_to_id.py +11 -0
- goosebit/db/migrations/models/5_20250619090242_null_feed.py +83 -0
- goosebit/db/models.py +19 -8
- goosebit/db/pg_ssl_context.py +51 -0
- goosebit/device_manager.py +262 -0
- goosebit/plugins/__init__.py +32 -0
- goosebit/schema/devices.py +8 -5
- goosebit/schema/plugins.py +67 -0
- goosebit/schema/updates.py +15 -0
- goosebit/schema/users.py +9 -0
- goosebit/settings/__init__.py +0 -3
- goosebit/settings/schema.py +60 -14
- goosebit/storage/__init__.py +62 -0
- goosebit/storage/base.py +14 -0
- goosebit/storage/filesystem.py +111 -0
- goosebit/storage/s3.py +104 -0
- goosebit/ui/bff/common/columns.py +50 -0
- goosebit/ui/bff/common/responses.py +1 -0
- goosebit/ui/bff/devices/device/__init__.py +1 -0
- goosebit/ui/bff/devices/device/routes.py +17 -0
- goosebit/ui/bff/devices/requests.py +1 -0
- goosebit/ui/bff/devices/routes.py +49 -46
- goosebit/ui/bff/download/routes.py +14 -3
- goosebit/ui/bff/rollouts/routes.py +32 -4
- goosebit/ui/bff/routes.py +2 -1
- goosebit/ui/bff/settings/__init__.py +1 -0
- goosebit/ui/bff/settings/routes.py +20 -0
- goosebit/ui/bff/settings/users/__init__.py +1 -0
- goosebit/ui/bff/settings/users/responses.py +33 -0
- goosebit/ui/bff/settings/users/routes.py +80 -0
- goosebit/ui/bff/software/routes.py +40 -12
- goosebit/ui/nav.py +12 -2
- goosebit/ui/routes.py +66 -13
- goosebit/ui/static/js/devices.js +32 -24
- goosebit/ui/static/js/login.js +21 -5
- goosebit/ui/static/js/logs.js +7 -22
- goosebit/ui/static/js/rollouts.js +31 -30
- goosebit/ui/static/js/settings.js +322 -0
- goosebit/ui/static/js/setup.js +28 -0
- goosebit/ui/static/js/software.js +127 -121
- goosebit/ui/static/js/util.js +25 -4
- goosebit/ui/templates/__init__.py +10 -1
- goosebit/ui/templates/login.html.jinja +5 -0
- goosebit/ui/templates/nav.html.jinja +13 -5
- goosebit/ui/templates/rollouts.html.jinja +4 -22
- goosebit/ui/templates/settings.html.jinja +88 -0
- goosebit/ui/templates/setup.html.jinja +71 -0
- goosebit/ui/templates/software.html.jinja +0 -11
- goosebit/updater/controller/v1/routes.py +119 -77
- goosebit/updater/routes.py +83 -8
- goosebit/updates/__init__.py +24 -31
- goosebit/updates/swdesc.py +15 -8
- goosebit/users/__init__.py +63 -0
- goosebit/util/__init__.py +0 -0
- goosebit/util/path.py +42 -0
- goosebit/util/version.py +92 -0
- goosebit-0.2.7.dist-info/METADATA +280 -0
- goosebit-0.2.7.dist-info/RECORD +133 -0
- {goosebit-0.2.5.dist-info → goosebit-0.2.7.dist-info}/WHEEL +1 -1
- goosebit/realtime/logs.py +0 -42
- goosebit/realtime/routes.py +0 -13
- goosebit/updater/manager.py +0 -325
- goosebit-0.2.5.dist-info/METADATA +0 -189
- goosebit-0.2.5.dist-info/RECORD +0 -99
- /goosebit/{realtime → api/v1/settings}/__init__.py +0 -0
- {goosebit-0.2.5.dist-info → goosebit-0.2.7.dist-info}/LICENSE +0 -0
- {goosebit-0.2.5.dist-info → goosebit-0.2.7.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,80 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import Annotated
|
4
|
+
|
5
|
+
from fastapi import APIRouter, Depends, Security
|
6
|
+
from tortoise.expressions import Q
|
7
|
+
|
8
|
+
from goosebit.api.v1.settings.users import routes
|
9
|
+
from goosebit.auth import validate_user_permissions
|
10
|
+
from goosebit.auth.permissions import GOOSEBIT_PERMISSIONS
|
11
|
+
from goosebit.db.models import User
|
12
|
+
from goosebit.ui.bff.common.columns import SettingsUsersColumns
|
13
|
+
from goosebit.ui.bff.common.requests import DataTableRequest
|
14
|
+
from goosebit.ui.bff.common.responses import DTColumns
|
15
|
+
from goosebit.ui.bff.common.util import parse_datatables_query
|
16
|
+
from goosebit.ui.bff.settings.users.responses import BFFSettingsUsersResponse
|
17
|
+
|
18
|
+
router = APIRouter(prefix="/users")
|
19
|
+
|
20
|
+
|
21
|
+
@router.get(
|
22
|
+
"",
|
23
|
+
dependencies=[Security(validate_user_permissions, scopes=[GOOSEBIT_PERMISSIONS["settings"]["users"]["read"]()])],
|
24
|
+
)
|
25
|
+
async def settings_users_get(
|
26
|
+
dt_query: Annotated[DataTableRequest, Depends(parse_datatables_query)],
|
27
|
+
) -> BFFSettingsUsersResponse:
|
28
|
+
filters: list[Q] = []
|
29
|
+
|
30
|
+
def search_filter(search_value):
|
31
|
+
base_filter = Q(Q(username__icontains=search_value), join_type="OR")
|
32
|
+
return Q(base_filter, *filters, join_type="AND")
|
33
|
+
|
34
|
+
query = User.all()
|
35
|
+
|
36
|
+
return await BFFSettingsUsersResponse.convert(dt_query, query, search_filter)
|
37
|
+
|
38
|
+
|
39
|
+
router.add_api_route(
|
40
|
+
"",
|
41
|
+
routes.settings_users_put,
|
42
|
+
methods=["POST"],
|
43
|
+
dependencies=[Security(validate_user_permissions, scopes=[GOOSEBIT_PERMISSIONS["settings"]["users"]["write"]()])],
|
44
|
+
name="bff_settings_users_put",
|
45
|
+
)
|
46
|
+
|
47
|
+
router.add_api_route(
|
48
|
+
"",
|
49
|
+
routes.settings_users_delete,
|
50
|
+
methods=["DELETE"],
|
51
|
+
dependencies=[Security(validate_user_permissions, scopes=[GOOSEBIT_PERMISSIONS["settings"]["users"]["delete"]()])],
|
52
|
+
name="bff_settings_users_delete",
|
53
|
+
)
|
54
|
+
|
55
|
+
router.add_api_route(
|
56
|
+
"",
|
57
|
+
routes.settings_users_patch,
|
58
|
+
methods=["PATCH"],
|
59
|
+
dependencies=[Security(validate_user_permissions, scopes=[GOOSEBIT_PERMISSIONS["settings"]["users"]["write"]()])],
|
60
|
+
name="bff_settings_users_patch",
|
61
|
+
)
|
62
|
+
|
63
|
+
|
64
|
+
@router.get(
|
65
|
+
"/columns",
|
66
|
+
dependencies=[Security(validate_user_permissions, scopes=[GOOSEBIT_PERMISSIONS["settings"]["users"]["read"]()])],
|
67
|
+
response_model_exclude_none=True,
|
68
|
+
)
|
69
|
+
async def settings_users_get_columns() -> DTColumns:
|
70
|
+
columns = list(
|
71
|
+
filter(
|
72
|
+
None,
|
73
|
+
[
|
74
|
+
SettingsUsersColumns.username,
|
75
|
+
SettingsUsersColumns.enabled,
|
76
|
+
SettingsUsersColumns.permissions,
|
77
|
+
],
|
78
|
+
)
|
79
|
+
)
|
80
|
+
return DTColumns(columns=columns)
|
@@ -9,12 +9,16 @@ from tortoise.expressions import Q
|
|
9
9
|
|
10
10
|
from goosebit.api.v1.software import routes
|
11
11
|
from goosebit.auth import validate_user_permissions
|
12
|
+
from goosebit.auth.permissions import GOOSEBIT_PERMISSIONS
|
12
13
|
from goosebit.db.models import Hardware, Rollout, Software
|
13
|
-
from goosebit.
|
14
|
+
from goosebit.storage import storage
|
14
15
|
from goosebit.ui.bff.common.requests import DataTableRequest
|
15
16
|
from goosebit.ui.bff.common.util import parse_datatables_query
|
16
17
|
from goosebit.updates import create_software_update
|
18
|
+
from goosebit.util.path import validate_filename
|
17
19
|
|
20
|
+
from ..common.columns import SoftwareColumns
|
21
|
+
from ..common.responses import DTColumns
|
18
22
|
from .responses import BFFSoftwareResponse
|
19
23
|
|
20
24
|
router = APIRouter(prefix="/software")
|
@@ -22,11 +26,11 @@ router = APIRouter(prefix="/software")
|
|
22
26
|
|
23
27
|
@router.get(
|
24
28
|
"",
|
25
|
-
dependencies=[Security(validate_user_permissions, scopes=["software
|
29
|
+
dependencies=[Security(validate_user_permissions, scopes=[GOOSEBIT_PERMISSIONS["software"]["read"]()])],
|
26
30
|
)
|
27
31
|
async def software_get(
|
28
32
|
dt_query: Annotated[DataTableRequest, Depends(parse_datatables_query)],
|
29
|
-
|
33
|
+
ids: list[str] = Query(default=None),
|
30
34
|
) -> BFFSoftwareResponse:
|
31
35
|
filters: list[Q] = []
|
32
36
|
|
@@ -36,8 +40,8 @@ async def software_get(
|
|
36
40
|
|
37
41
|
query = Software.all().prefetch_related("compatibility")
|
38
42
|
|
39
|
-
if
|
40
|
-
hardware = await Hardware.filter(
|
43
|
+
if ids:
|
44
|
+
hardware = await Hardware.filter(devices__id__in=ids).distinct()
|
41
45
|
filters.append(Q(*[Q(compatibility__id=c.id) for c in hardware], join_type="AND"))
|
42
46
|
|
43
47
|
return await BFFSoftwareResponse.convert(dt_query, query, search_filter, Q(*filters))
|
@@ -47,14 +51,14 @@ router.add_api_route(
|
|
47
51
|
"",
|
48
52
|
routes.software_delete,
|
49
53
|
methods=["DELETE"],
|
50
|
-
dependencies=[Security(validate_user_permissions, scopes=["software
|
54
|
+
dependencies=[Security(validate_user_permissions, scopes=[GOOSEBIT_PERMISSIONS["software"]["delete"]()])],
|
51
55
|
name="bff_software_delete",
|
52
56
|
)
|
53
57
|
|
54
58
|
|
55
59
|
@router.post(
|
56
60
|
"",
|
57
|
-
dependencies=[Security(validate_user_permissions, scopes=["software
|
61
|
+
dependencies=[Security(validate_user_permissions, scopes=[GOOSEBIT_PERMISSIONS["software"]["write"]()])],
|
58
62
|
)
|
59
63
|
async def post_update(
|
60
64
|
request: Request,
|
@@ -77,11 +81,14 @@ async def post_update(
|
|
77
81
|
await create_software_update(url, None)
|
78
82
|
else:
|
79
83
|
# local file
|
80
|
-
|
81
|
-
file = artifacts_dir.joinpath(filename)
|
82
|
-
await artifacts_dir.mkdir(parents=True, exist_ok=True)
|
84
|
+
temp_dir = Path(storage.get_temp_dir())
|
83
85
|
|
84
|
-
|
86
|
+
try:
|
87
|
+
file_path = await validate_filename(filename, temp_dir)
|
88
|
+
except ValueError as e:
|
89
|
+
raise HTTPException(400, f"Invalid filename: {e}")
|
90
|
+
|
91
|
+
temp_file = file_path.with_suffix(".tmp")
|
85
92
|
if init:
|
86
93
|
await temp_file.unlink(missing_ok=True)
|
87
94
|
|
@@ -90,7 +97,28 @@ async def post_update(
|
|
90
97
|
|
91
98
|
if done:
|
92
99
|
try:
|
93
|
-
absolute = await
|
100
|
+
absolute = await file_path.absolute()
|
94
101
|
await create_software_update(absolute.as_uri(), temp_file)
|
95
102
|
finally:
|
96
103
|
await temp_file.unlink(missing_ok=True)
|
104
|
+
|
105
|
+
|
106
|
+
@router.get(
|
107
|
+
"/columns",
|
108
|
+
dependencies=[Security(validate_user_permissions, scopes=[GOOSEBIT_PERMISSIONS["software"]["read"]()])],
|
109
|
+
response_model_exclude_none=True,
|
110
|
+
)
|
111
|
+
async def devices_get_columns() -> DTColumns:
|
112
|
+
columns = list(
|
113
|
+
filter(
|
114
|
+
None,
|
115
|
+
[
|
116
|
+
SoftwareColumns.id,
|
117
|
+
SoftwareColumns.name,
|
118
|
+
SoftwareColumns.version,
|
119
|
+
SoftwareColumns.compatibility,
|
120
|
+
SoftwareColumns.size,
|
121
|
+
],
|
122
|
+
)
|
123
|
+
)
|
124
|
+
return DTColumns(columns=columns)
|
goosebit/ui/nav.py
CHANGED
@@ -1,10 +1,20 @@
|
|
1
|
+
from pydantic import BaseModel
|
2
|
+
|
3
|
+
|
4
|
+
class NavigationItem(BaseModel):
|
5
|
+
function: str
|
6
|
+
text: str
|
7
|
+
permissions: list[str]
|
8
|
+
show: bool
|
9
|
+
|
10
|
+
|
1
11
|
class Navigation:
|
2
12
|
def __init__(self):
|
3
13
|
self.items = []
|
4
14
|
|
5
|
-
def route(self, text: str, permissions: str | None = None):
|
15
|
+
def route(self, text: str, permissions: list[str] | None = None, show: bool = True):
|
6
16
|
def decorator(func):
|
7
|
-
self.items.append(
|
17
|
+
self.items.append(NavigationItem(function=func.__name__, text=text, permissions=permissions, show=show))
|
8
18
|
return func
|
9
19
|
|
10
20
|
return decorator
|
goosebit/ui/routes.py
CHANGED
@@ -1,19 +1,48 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
import logging
|
2
|
+
|
3
|
+
from fastapi import APIRouter, Depends, HTTPException, Security
|
4
|
+
from fastapi.requests import HTTPConnection, Request
|
3
5
|
from fastapi.responses import RedirectResponse
|
4
|
-
from fastapi.security import
|
6
|
+
from fastapi.security import SecurityScopes
|
5
7
|
|
6
|
-
from goosebit.auth import
|
8
|
+
from goosebit.auth import (
|
9
|
+
check_permissions,
|
10
|
+
get_current_user,
|
11
|
+
redirect_if_unauthenticated,
|
12
|
+
)
|
13
|
+
from goosebit.auth.permissions import GOOSEBIT_PERMISSIONS
|
7
14
|
from goosebit.ui.nav import nav
|
8
15
|
|
16
|
+
from ..db.models import User
|
9
17
|
from . import bff
|
10
18
|
from .templates import templates
|
11
19
|
|
12
|
-
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")
|
13
|
-
|
14
20
|
router = APIRouter(prefix="/ui", include_in_schema=False)
|
15
21
|
router.include_router(bff.router)
|
16
22
|
|
23
|
+
logger = logging.getLogger(__name__)
|
24
|
+
|
25
|
+
|
26
|
+
def validate_user_permissions_with_nav_redirect(
|
27
|
+
connection: HTTPConnection,
|
28
|
+
security: SecurityScopes,
|
29
|
+
user: User = Depends(get_current_user),
|
30
|
+
):
|
31
|
+
if not check_permissions(security.scopes, user.permissions):
|
32
|
+
logger.warning(f"{user.username} does not have sufficient permissions")
|
33
|
+
for item in nav.items:
|
34
|
+
if check_permissions(item.permissions, user.permissions):
|
35
|
+
raise HTTPException(
|
36
|
+
status_code=302,
|
37
|
+
headers={"location": str(connection.url_for(item.function))},
|
38
|
+
)
|
39
|
+
raise HTTPException(
|
40
|
+
status_code=403,
|
41
|
+
detail="Not enough permissions",
|
42
|
+
headers={"WWW-Authenticate": "Bearer"},
|
43
|
+
)
|
44
|
+
return connection
|
45
|
+
|
17
46
|
|
18
47
|
@router.get("", dependencies=[Depends(redirect_if_unauthenticated)])
|
19
48
|
async def ui_root(request: Request):
|
@@ -22,34 +51,58 @@ async def ui_root(request: Request):
|
|
22
51
|
|
23
52
|
@router.get(
|
24
53
|
"/devices",
|
25
|
-
dependencies=[
|
54
|
+
dependencies=[
|
55
|
+
Depends(redirect_if_unauthenticated),
|
56
|
+
Security(validate_user_permissions_with_nav_redirect, scopes=[GOOSEBIT_PERMISSIONS["device"]["read"]()]),
|
57
|
+
],
|
26
58
|
)
|
27
|
-
@nav.route("Devices", permissions="device
|
59
|
+
@nav.route("Devices", permissions=[GOOSEBIT_PERMISSIONS["device"]["read"]()])
|
28
60
|
async def devices_ui(request: Request):
|
29
61
|
return templates.TemplateResponse(request, "devices.html.jinja", context={"title": "Devices"})
|
30
62
|
|
31
63
|
|
32
64
|
@router.get(
|
33
65
|
"/software",
|
34
|
-
dependencies=[
|
66
|
+
dependencies=[
|
67
|
+
Depends(redirect_if_unauthenticated),
|
68
|
+
Security(validate_user_permissions_with_nav_redirect, scopes=[GOOSEBIT_PERMISSIONS["software"]["read"]()]),
|
69
|
+
],
|
35
70
|
)
|
36
|
-
@nav.route("Software", permissions="software
|
71
|
+
@nav.route("Software", permissions=[GOOSEBIT_PERMISSIONS["software"]["read"]()])
|
37
72
|
async def software_ui(request: Request):
|
38
73
|
return templates.TemplateResponse(request, "software.html.jinja", context={"title": "Software"})
|
39
74
|
|
40
75
|
|
41
76
|
@router.get(
|
42
77
|
"/rollouts",
|
43
|
-
dependencies=[
|
78
|
+
dependencies=[
|
79
|
+
Depends(redirect_if_unauthenticated),
|
80
|
+
Security(validate_user_permissions_with_nav_redirect, scopes=[GOOSEBIT_PERMISSIONS["rollout"]["read"]()]),
|
81
|
+
],
|
44
82
|
)
|
45
|
-
@nav.route("Rollouts", permissions="rollout
|
83
|
+
@nav.route("Rollouts", permissions=[GOOSEBIT_PERMISSIONS["rollout"]["read"]()])
|
46
84
|
async def rollouts_ui(request: Request):
|
47
85
|
return templates.TemplateResponse(request, "rollouts.html.jinja", context={"title": "Rollouts"})
|
48
86
|
|
49
87
|
|
50
88
|
@router.get(
|
51
89
|
"/logs/{dev_id}",
|
52
|
-
dependencies=[
|
90
|
+
dependencies=[
|
91
|
+
Depends(redirect_if_unauthenticated),
|
92
|
+
Security(validate_user_permissions_with_nav_redirect, scopes=[GOOSEBIT_PERMISSIONS["device"]["read"]()]),
|
93
|
+
],
|
53
94
|
)
|
54
95
|
async def logs_ui(request: Request, dev_id: str):
|
55
96
|
return templates.TemplateResponse(request, "logs.html.jinja", context={"title": "Log", "device": dev_id})
|
97
|
+
|
98
|
+
|
99
|
+
@router.get(
|
100
|
+
"/settings",
|
101
|
+
dependencies=[
|
102
|
+
Depends(redirect_if_unauthenticated),
|
103
|
+
Security(validate_user_permissions_with_nav_redirect, scopes=[GOOSEBIT_PERMISSIONS["settings"]()]),
|
104
|
+
],
|
105
|
+
)
|
106
|
+
@nav.route("Settings", permissions=[GOOSEBIT_PERMISSIONS["settings"]()], show=False)
|
107
|
+
async def settings_ui(request: Request):
|
108
|
+
return templates.TemplateResponse(request, "settings.html.jinja", context={"title": "Settings"})
|
goosebit/ui/static/js/devices.js
CHANGED
@@ -1,19 +1,8 @@
|
|
1
1
|
let dataTable;
|
2
2
|
|
3
3
|
const renderFunctions = {
|
4
|
-
online: (data, type) => {
|
5
|
-
if (type === "display" || type === "filter") {
|
6
|
-
const color = data ? "success" : "danger";
|
7
|
-
return `
|
8
|
-
<div class="text-${color}">
|
9
|
-
●
|
10
|
-
</div>
|
11
|
-
`;
|
12
|
-
}
|
13
|
-
return data;
|
14
|
-
},
|
15
4
|
force_update: (data, type) => {
|
16
|
-
if (type === "display"
|
5
|
+
if (type === "display") {
|
17
6
|
const color = data ? "success" : "muted";
|
18
7
|
return `
|
19
8
|
<div class="text-${color}">
|
@@ -29,6 +18,9 @@ const renderFunctions = {
|
|
29
18
|
}
|
30
19
|
return data;
|
31
20
|
},
|
21
|
+
polling: (data, type) => {
|
22
|
+
return data ? "on time" : "overdue";
|
23
|
+
},
|
32
24
|
last_seen: (data, type) => {
|
33
25
|
if (type === "display" || type === "filter") {
|
34
26
|
return secondsToRecentDate(data);
|
@@ -52,13 +44,13 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
52
44
|
paging: true,
|
53
45
|
processing: false,
|
54
46
|
serverSide: true,
|
55
|
-
order: { name: "
|
47
|
+
order: { name: "id", dir: "asc" },
|
56
48
|
scrollCollapse: true,
|
57
49
|
scroller: true,
|
58
50
|
scrollY: "65vh",
|
59
51
|
stateSave: true,
|
60
52
|
select: true,
|
61
|
-
rowId: "
|
53
|
+
rowId: "id",
|
62
54
|
ajax: {
|
63
55
|
url: "/ui/bff/devices",
|
64
56
|
data: (data) => {
|
@@ -96,7 +88,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
96
88
|
text: '<i class="bi bi-file-text"></i>',
|
97
89
|
action: () => {
|
98
90
|
const selectedDevice = dataTable.rows({ selected: true }).data().toArray()[0];
|
99
|
-
window.location.href = `/ui/logs/${selectedDevice.
|
91
|
+
window.location.href = `/ui/logs/${selectedDevice.id}`;
|
100
92
|
},
|
101
93
|
className: "buttons-logs",
|
102
94
|
titleAttr: "View Log",
|
@@ -125,7 +117,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
125
117
|
.rows({ selected: true })
|
126
118
|
.data()
|
127
119
|
.toArray()
|
128
|
-
.map((d) => d.
|
120
|
+
.map((d) => d.id);
|
129
121
|
await deleteDevices(selectedDevices);
|
130
122
|
},
|
131
123
|
className: "buttons-delete",
|
@@ -138,7 +130,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
138
130
|
.rows({ selected: true })
|
139
131
|
.data()
|
140
132
|
.toArray()
|
141
|
-
.map((d) => d.
|
133
|
+
.map((d) => d.id);
|
142
134
|
await forceUpdateDevices(selectedDevices);
|
143
135
|
},
|
144
136
|
className: "buttons-force-update",
|
@@ -151,7 +143,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
151
143
|
.rows({ selected: true })
|
152
144
|
.data()
|
153
145
|
.toArray()
|
154
|
-
.map((d) => d.
|
146
|
+
.map((d) => d.id);
|
155
147
|
await pinDevices(selectedDevices);
|
156
148
|
},
|
157
149
|
className: "buttons-pin",
|
@@ -171,7 +163,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
171
163
|
});
|
172
164
|
|
173
165
|
setInterval(() => {
|
174
|
-
|
166
|
+
updateDeviceList();
|
175
167
|
}, TABLE_UPDATE_TIME);
|
176
168
|
|
177
169
|
await updateSoftwareSelection();
|
@@ -291,7 +283,7 @@ async function updateDeviceName() {
|
|
291
283
|
.rows({ selected: true })
|
292
284
|
.data()
|
293
285
|
.toArray()
|
294
|
-
.map((d) => d.
|
286
|
+
.map((d) => d.id);
|
295
287
|
const name = document.getElementById("device-name").value;
|
296
288
|
|
297
289
|
try {
|
@@ -308,7 +300,7 @@ async function updateDeviceRollout() {
|
|
308
300
|
.rows({ selected: true })
|
309
301
|
.data()
|
310
302
|
.toArray()
|
311
|
-
.map((d) => d.
|
303
|
+
.map((d) => d.id);
|
312
304
|
const feed = document.getElementById("device-selected-feed").value;
|
313
305
|
const software = "rollout";
|
314
306
|
|
@@ -326,7 +318,7 @@ async function updateDeviceManualSoftware() {
|
|
326
318
|
.rows({ selected: true })
|
327
319
|
.data()
|
328
320
|
.toArray()
|
329
|
-
.map((d) => d.
|
321
|
+
.map((d) => d.id);
|
330
322
|
const feed = null;
|
331
323
|
const software = document.getElementById("selected-sw").value;
|
332
324
|
|
@@ -344,7 +336,7 @@ async function updateDeviceLatest() {
|
|
344
336
|
.rows({ selected: true })
|
345
337
|
.data()
|
346
338
|
.toArray()
|
347
|
-
.map((d) => d.
|
339
|
+
.map((d) => d.id);
|
348
340
|
const feed = null;
|
349
341
|
const software = "latest";
|
350
342
|
|
@@ -388,5 +380,21 @@ async function pinDevices(devices) {
|
|
388
380
|
}
|
389
381
|
|
390
382
|
function updateDeviceList() {
|
391
|
-
|
383
|
+
const scrollPosition = $("#device-table").parent().scrollTop(); // Get current scroll position
|
384
|
+
|
385
|
+
const selectedRows = dataTable
|
386
|
+
.rows({ selected: true })
|
387
|
+
.data()
|
388
|
+
.toArray()
|
389
|
+
.map((d) => d.id);
|
390
|
+
|
391
|
+
dataTable.ajax.reload(() => {
|
392
|
+
dataTable.rows().every(function () {
|
393
|
+
const rowData = this.data();
|
394
|
+
if (selectedRows.includes(rowData.id)) {
|
395
|
+
this.select();
|
396
|
+
}
|
397
|
+
});
|
398
|
+
$("#device-table").parent().scrollTop(scrollPosition); // Restore scroll position after reload
|
399
|
+
}, false);
|
392
400
|
}
|
goosebit/ui/static/js/login.js
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
loginForm = document.getElementById("login_form");
|
1
|
+
const loginForm = document.getElementById("login_form");
|
2
|
+
const errorContainer = document.getElementById("login_error");
|
2
3
|
|
3
4
|
async function login() {
|
4
5
|
const formData = new FormData(loginForm);
|
@@ -8,16 +9,31 @@ async function login() {
|
|
8
9
|
method: "POST",
|
9
10
|
body: formData,
|
10
11
|
});
|
11
|
-
|
12
|
+
|
13
|
+
if (response.status === 401) {
|
14
|
+
const result = await response.json();
|
15
|
+
errorContainer.textContent = result.detail || "Failed to login.";
|
16
|
+
errorContainer.classList.remove("d-none");
|
17
|
+
return;
|
18
|
+
}
|
19
|
+
|
20
|
+
if (!response.ok) {
|
21
|
+
errorContainer.textContent = "Something went wrong. Please try again.";
|
22
|
+
errorContainer.classList.remove("d-none");
|
23
|
+
return;
|
24
|
+
}
|
25
|
+
|
26
|
+
const tokenData = await response.json();
|
12
27
|
document.cookie = `session_id=${tokenData.access_token}; path=/`;
|
13
28
|
location.reload();
|
14
|
-
} catch (
|
15
|
-
|
16
|
-
|
29
|
+
} catch (error) {
|
30
|
+
errorContainer.textContent = "Network error. Please try again.";
|
31
|
+
errorContainer.classList.remove("d-none");
|
17
32
|
}
|
18
33
|
}
|
19
34
|
|
20
35
|
loginForm.addEventListener("submit", (event) => {
|
21
36
|
event.preventDefault();
|
37
|
+
errorContainer.classList.add("d-none"); // Hide error before new attempt
|
22
38
|
login();
|
23
39
|
});
|
goosebit/ui/static/js/logs.js
CHANGED
@@ -1,25 +1,10 @@
|
|
1
|
-
document.addEventListener("DOMContentLoaded", () => {
|
2
|
-
const
|
1
|
+
document.addEventListener("DOMContentLoaded", async () => {
|
2
|
+
const res = await get_request(`/ui/bff/devices/${device}/log`);
|
3
3
|
|
4
|
-
|
5
|
-
|
4
|
+
const logElem = document.getElementById("device-log");
|
5
|
+
logElem.textContent = res.log;
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
}
|
11
|
-
logElem.textContent += res.log;
|
12
|
-
|
13
|
-
const progressElem = document.getElementById("install-progress");
|
14
|
-
progressElem.style.width = `${res.progress}%`;
|
15
|
-
progressElem.innerHTML = `${res.progress}%`;
|
16
|
-
});
|
7
|
+
const progressElem = document.getElementById("install-progress");
|
8
|
+
progressElem.style.width = `${res.progress}%`;
|
9
|
+
progressElem.innerHTML = `${res.progress}%`;
|
17
10
|
});
|
18
|
-
|
19
|
-
function create_ws(s) {
|
20
|
-
const l = window.location;
|
21
|
-
const protocol = l.protocol === "https:" ? "wss://" : "ws://";
|
22
|
-
const port = l.port !== "80" || l.port !== "443" ? l.port : "";
|
23
|
-
const url = `${protocol}${l.hostname}:${port}${s}`;
|
24
|
-
return new WebSocket(url);
|
25
|
-
}
|
@@ -1,6 +1,30 @@
|
|
1
1
|
let dataTable;
|
2
2
|
|
3
|
+
const renderFunctions = {
|
4
|
+
paused: (data, type) => {
|
5
|
+
if (type === "display") {
|
6
|
+
const color = data ? "danger" : "muted";
|
7
|
+
return `
|
8
|
+
<div class="text-${color}">
|
9
|
+
●
|
10
|
+
</div>
|
11
|
+
`;
|
12
|
+
}
|
13
|
+
return data;
|
14
|
+
},
|
15
|
+
created_at: (data, type) => new Date(data).toLocaleString(),
|
16
|
+
};
|
17
|
+
|
3
18
|
document.addEventListener("DOMContentLoaded", async () => {
|
19
|
+
const columnConfig = await get_request("/ui/bff/rollouts/columns");
|
20
|
+
for (const col in columnConfig.columns) {
|
21
|
+
const colDesc = columnConfig.columns[col];
|
22
|
+
const colName = colDesc.data;
|
23
|
+
if (renderFunctions[colName]) {
|
24
|
+
columnConfig.columns[col].render = renderFunctions[colName];
|
25
|
+
}
|
26
|
+
}
|
27
|
+
|
4
28
|
dataTable = new DataTable("#rollout-table", {
|
5
29
|
responsive: true,
|
6
30
|
paging: true,
|
@@ -32,37 +56,10 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
32
56
|
targets: "_all",
|
33
57
|
searchable: false,
|
34
58
|
orderable: false,
|
59
|
+
render: (data) => data || "-",
|
35
60
|
},
|
36
61
|
],
|
37
|
-
columns:
|
38
|
-
{ data: "id", visible: false },
|
39
|
-
{
|
40
|
-
data: "created_at",
|
41
|
-
name: "created_at",
|
42
|
-
orderable: true,
|
43
|
-
render: (data) => new Date(data).toLocaleString(),
|
44
|
-
},
|
45
|
-
{ data: "name", name: "name", searchable: true, orderable: true },
|
46
|
-
{ data: "feed", name: "feed", searchable: true, orderable: true },
|
47
|
-
{ data: "sw_file" },
|
48
|
-
{ data: "sw_version" },
|
49
|
-
{
|
50
|
-
data: "paused",
|
51
|
-
render: (data, type) => {
|
52
|
-
if (type === "display" || type === "filter") {
|
53
|
-
const color = data ? "danger" : "muted";
|
54
|
-
return `
|
55
|
-
<div class="text-${color}">
|
56
|
-
●
|
57
|
-
</div>
|
58
|
-
`;
|
59
|
-
}
|
60
|
-
return data;
|
61
|
-
},
|
62
|
-
},
|
63
|
-
{ data: "success_count" },
|
64
|
-
{ data: "failure_count" },
|
65
|
-
],
|
62
|
+
columns: columnConfig.columns,
|
66
63
|
layout: {
|
67
64
|
top1Start: {
|
68
65
|
buttons: [],
|
@@ -194,7 +191,11 @@ async function createRollout() {
|
|
194
191
|
}
|
195
192
|
|
196
193
|
function updateRolloutList() {
|
197
|
-
|
194
|
+
const scrollPosition = $("#rollout-table").parent().scrollTop(); // Get current scroll position
|
195
|
+
|
196
|
+
dataTable.ajax.reload(() => {
|
197
|
+
$("#rollout-table").parent().scrollTop(scrollPosition); // Restore scroll position after reload
|
198
|
+
}, false);
|
198
199
|
}
|
199
200
|
|
200
201
|
async function deleteRollouts(ids) {
|