Skip to content

Commit

Permalink
Merge pull request #138 from UpstreamDataInc/dev_selects
Browse files Browse the repository at this point in the history
  • Loading branch information
b-rowan authored Sep 26, 2024
2 parents e59bc2a + 1a954a4 commit 853d8b7
Show file tree
Hide file tree
Showing 13 changed files with 332 additions and 105 deletions.
9 changes: 8 additions & 1 deletion goosebit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
from logging import getLogger
from typing import Annotated

from fastapi import Depends, FastAPI
from fastapi import Depends, FastAPI, HTTPException
from fastapi.openapi.docs import get_swagger_ui_html
from fastapi.requests import Request
from fastapi.responses import RedirectResponse
from fastapi.security import OAuth2PasswordRequestForm
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor as Instrumentor
from tortoise.exceptions import ValidationError

from goosebit import api, db, realtime, ui, updater
from goosebit.api.telemetry import metrics
Expand Down Expand Up @@ -58,6 +59,12 @@ async def lifespan(_: FastAPI):
Instrumentor.instrument_app(app)


# Custom exception handler for Tortoise ValidationError
@app.exception_handler(ValidationError)
async def tortoise_validation_exception_handler(request: Request, exc: ValidationError):
raise HTTPException(422, str(exc))


@app.middleware("http")
async def attach_user(request: Request, call_next):
request.scope["user"] = await get_user_from_request(request)
Expand Down
9 changes: 9 additions & 0 deletions goosebit/db/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import semver
from anyio import Path
from tortoise import Model, fields
from tortoise.exceptions import ValidationError

from goosebit.api.telemetry.metrics import devices_count

Expand Down Expand Up @@ -75,6 +76,14 @@ class Device(Model):
tags = fields.ManyToManyField("models.Tag", related_name="devices", through="device_tags")

async def save(self, *args, **kwargs):
# Check if the software is compatible with the hardware before saving
if self.assigned_software and self.hardware:
# Check if the assigned software is compatible with the hardware
await self.fetch_related("assigned_software", "hardware")
is_compatible = await self.assigned_software.compatibility.filter(id=self.hardware.id).exists()
if not is_compatible:
raise ValidationError("The assigned software is not compatible with the device's hardware.")

is_new = self._saved_in_db is False
await super().save(*args, **kwargs)
if is_new:
Expand Down
2 changes: 1 addition & 1 deletion goosebit/ui/bff/common/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class DataTableRequest(BaseModel):
columns: list[DataTableColumnSchema] = list()
order: list[DataTableOrderSchema] = list()
start: int = 0
length: int = 10
length: int = 0
search: DataTableSearchSchema = DataTableSearchSchema()

@computed_field # type: ignore[misc]
Expand Down
2 changes: 1 addition & 1 deletion goosebit/ui/bff/devices/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def search_filter(search_value: str):
| Q(last_state=int(UpdateStateEnum.from_str(search_value)))
)

query = Device.all().prefetch_related("assigned_software", "hardware")
query = Device.all().prefetch_related("assigned_software", "hardware", "assigned_software__compatibility")

return await BFFDeviceResponse.convert(dt_query, query, search_filter)

Expand Down
12 changes: 10 additions & 2 deletions goosebit/ui/bff/software/responses.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import Callable

from pydantic import BaseModel, Field
from tortoise.expressions import Q
from tortoise.queryset import QuerySet

from goosebit.schema.software import SoftwareSchema
Expand All @@ -14,16 +15,23 @@ class BFFSoftwareResponse(BaseModel):
records_filtered: int = Field(serialization_alias="recordsFiltered")

@classmethod
async def convert(cls, dt_query: DataTableRequest, query: QuerySet, search_filter: Callable):
async def convert(cls, dt_query: DataTableRequest, query: QuerySet, search_filter: Callable, alt_filter: Q):
total_records = await query.count()
query = query.filter(alt_filter)
if dt_query.search.value:
query = query.filter(search_filter(dt_query.search.value))

if dt_query.order_query:
query = query.order_by(dt_query.order_query)

filtered_records = await query.count()
devices = await query.offset(dt_query.start).limit(dt_query.length).all()

query = query.offset(dt_query.start)

if not dt_query.length == 0:
query = query.limit(dt_query.length)

devices = await query.all()
data = [SoftwareSchema.model_validate(d) for d in devices]

return cls(data=data, draw=dt_query.draw, records_total=total_records, records_filtered=filtered_records)
22 changes: 16 additions & 6 deletions goosebit/ui/bff/software/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
from typing import Annotated

from anyio import Path, open_file
from fastapi import APIRouter, Depends, Form, HTTPException, Security, UploadFile
from fastapi import APIRouter, Depends, Form, HTTPException, Query, Security, UploadFile
from fastapi.requests import Request
from tortoise.expressions import Q

from goosebit.api.v1.software import routes
from goosebit.auth import validate_user_permissions
from goosebit.db.models import Rollout, Software
from goosebit.db.models import Hardware, Rollout, Software
from goosebit.settings import config
from goosebit.ui.bff.common.requests import DataTableRequest
from goosebit.ui.bff.common.util import parse_datatables_query
Expand All @@ -24,13 +24,23 @@
"",
dependencies=[Security(validate_user_permissions, scopes=["software.read"])],
)
async def software_get(dt_query: Annotated[DataTableRequest, Depends(parse_datatables_query)]) -> BFFSoftwareResponse:
def search_filter(search_value: str):
return Q(uri__icontains=search_value) | Q(version__icontains=search_value)
async def software_get(
dt_query: Annotated[DataTableRequest, Depends(parse_datatables_query)],
uuids: list[str] = Query(default=None),
) -> BFFSoftwareResponse:
filters: list[Q] = []

def search_filter(search_value):
base_filter = Q(Q(uri__icontains=search_value), Q(version__icontains=search_value), join_type="OR")
return Q(base_filter, *filters, join_type="AND")

query = Software.all().prefetch_related("compatibility")

return await BFFSoftwareResponse.convert(dt_query, query, search_filter)
if uuids:
hardware = await Hardware.filter(devices__uuid__in=uuids).distinct()
filters.append(Q(*[Q(compatibility__id=c.id) for c in hardware], join_type="AND"))

return await BFFSoftwareResponse.convert(dt_query, query, search_filter, Q(*filters))


router.add_api_route(
Expand Down
160 changes: 135 additions & 25 deletions goosebit/ui/static/js/devices.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,20 +124,11 @@ document.addEventListener("DOMContentLoaded", async () => {
{
text: '<i class="bi bi-pen" ></i>',
action: () => {
const selectedDevice = dataTable.rows({ selected: true }).data().toArray()[0];
$("#device-selected-name").val(selectedDevice.name);
const selectedDevices = dataTable.rows({ selected: true }).data().toArray();
const selectedDevice = selectedDevices[0];
updateSoftwareSelection(selectedDevices);
$("#device-name").val(selectedDevice.name);
$("#device-selected-feed").val(selectedDevice.feed);

let selectedValue;
if (selectedDevice.update_mode === "Rollout") {
selectedValue = "rollout";
} else if (selectedDevice.update_mode === "Latest") {
selectedValue = "latest";
} else {
selectedValue = selectedDevice.sw_assigned;
}
$("#selected-sw").val(selectedValue);

new bootstrap.Modal("#device-config-modal").show();
},
className: "buttons-config",
Expand Down Expand Up @@ -199,22 +190,89 @@ document.addEventListener("DOMContentLoaded", async () => {
dataTable.ajax.reload(null, false);
}, TABLE_UPDATE_TIME);

await updateSoftwareSelection(true);
await updateSoftwareSelection();

// Name update form submit
const nameForm = document.getElementById("device-name-form");
nameForm.addEventListener(
"submit",
async (event) => {
if (nameForm.checkValidity() === false) {
event.preventDefault();
event.stopPropagation();
nameForm.classList.add("was-validated");
} else {
event.preventDefault();
await updateDeviceName();
nameForm.classList.remove("was-validated");
nameForm.reset();
const modal = bootstrap.Modal.getInstance(document.getElementById("device-config-modal"));
modal.hide();
}
},
false,
);

// Rollout form submit
const rolloutForm = document.getElementById("device-software-rollout-form");
rolloutForm.addEventListener(
"submit",
async (event) => {
if (rolloutForm.checkValidity() === false) {
event.preventDefault();
event.stopPropagation();
rolloutForm.classList.add("was-validated");
} else {
event.preventDefault();
await updateDeviceRollout();
rolloutForm.classList.remove("was-validated");
rolloutForm.reset();
const modal = bootstrap.Modal.getInstance(document.getElementById("device-config-modal"));
modal.hide();
}
},
false,
);

// Manual software form submit
const manualSoftwareForm = document.getElementById("device-software-manual-form");
manualSoftwareForm.addEventListener(
"submit",
async (event) => {
if (manualSoftwareForm.checkValidity() === false) {
event.preventDefault();
event.stopPropagation();
manualSoftwareForm.classList.add("was-validated");
if (document.getElementById("selected-sw").value === "") {
document.getElementById("selected-sw").parentElement.classList.add("is-invalid");
}
} else {
event.preventDefault();
await updateDeviceManualSoftware();
manualSoftwareForm.classList.remove("was-validated");
document.getElementById("selected-sw").parentElement.classList.remove("is-invalid");
manualSoftwareForm.reset();
const modal = bootstrap.Modal.getInstance(document.getElementById("device-config-modal"));
modal.hide();
}
},
false,
);

// Config form submit
const configForm = document.getElementById("device-config-form");
configForm.addEventListener(
// Latest software form submit
const latestSoftwareForm = document.getElementById("device-software-latest-form");
latestSoftwareForm.addEventListener(
"submit",
async (event) => {
if (configForm.checkValidity() === false) {
if (latestSoftwareForm.checkValidity() === false) {
event.preventDefault();
event.stopPropagation();
configForm.classList.add("was-validated");
latestSoftwareForm.classList.add("was-validated");
} else {
event.preventDefault();
await updateDeviceConfig();
configForm.classList.remove("was-validated");
configForm.reset();
await updateDeviceLatest();
latestSoftwareForm.classList.remove("was-validated");
latestSoftwareForm.reset();
const modal = bootstrap.Modal.getInstance(document.getElementById("device-config-modal"));
modal.hide();
}
Expand Down Expand Up @@ -244,18 +302,70 @@ function updateBtnState() {
}
}

async function updateDeviceConfig() {
async function updateDeviceName() {
const devices = dataTable
.rows({ selected: true })
.data()
.toArray()
.map((d) => d.uuid);
const name = document.getElementById("device-name").value;

try {
await patch_request("/ui/bff/devices", { devices, name });
} catch (error) {
console.error("Update device config failed:", error);
}

setTimeout(updateDeviceList, 50);
}

async function updateDeviceRollout() {
const devices = dataTable
.rows({ selected: true })
.data()
.toArray()
.map((d) => d.uuid);
const name = document.getElementById("device-selected-name").value;
const feed = document.getElementById("device-selected-feed").value;
const software = "rollout";

try {
await patch_request("/ui/bff/devices", { devices, feed, software });
} catch (error) {
console.error("Update device config failed:", error);
}

setTimeout(updateDeviceList, 50);
}

async function updateDeviceManualSoftware() {
const devices = dataTable
.rows({ selected: true })
.data()
.toArray()
.map((d) => d.uuid);
const feed = null;
const software = document.getElementById("selected-sw").value;

try {
await patch_request("/ui/bff/devices", { devices, name, feed, software });
await patch_request("/ui/bff/devices", { devices, feed, software });
} catch (error) {
console.error("Update device config failed:", error);
}

setTimeout(updateDeviceList, 50);
}

async function updateDeviceLatest() {
const devices = dataTable
.rows({ selected: true })
.data()
.toArray()
.map((d) => d.uuid);
const feed = null;
const software = "latest";

try {
await patch_request("/ui/bff/devices", { devices, feed, software });
} catch (error) {
console.error("Update device config failed:", error);
}
Expand Down
4 changes: 4 additions & 0 deletions goosebit/ui/static/js/rollouts.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,13 +136,17 @@ document.addEventListener("DOMContentLoaded", async () => {
"submit",
(event) => {
if (form.checkValidity() === false) {
if (document.getElementById("selected-sw").value === "") {
document.getElementById("selected-sw").parentElement.classList.add("is-invalid");
}
event.preventDefault();
event.stopPropagation();
form.classList.add("was-validated");
} else {
event.preventDefault();
createRollout();
form.classList.remove("was-validated");
document.getElementById("selected-sw").parentElement.classList.remove("is-invalid");
form.reset();
const modal = bootstrap.Modal.getInstance(document.getElementById("rollout-create-modal"));
modal.hide();
Expand Down
Loading

0 comments on commit 853d8b7

Please sign in to comment.