diff --git a/pscompose/api/api.py b/pscompose/api/api.py index 236f997..40101f7 100644 --- a/pscompose/api/api.py +++ b/pscompose/api/api.py @@ -11,6 +11,7 @@ tasks, templates, tests, + home, ) # initialize FastAPI application @@ -25,7 +26,18 @@ ) # include submodule routers -for lib in [addresses, archives, basic_auth, contexts, groups, schedules, tasks, templates, tests]: +for lib in [ + addresses, + archives, + basic_auth, + contexts, + groups, + schedules, + tasks, + templates, + tests, + home, +]: app.include_router(lib.router) diff --git a/pscompose/api/routers/addresses.py b/pscompose/api/routers/addresses.py index 6ba0fd9..663a7b7 100644 --- a/pscompose/api/routers/addresses.py +++ b/pscompose/api/routers/addresses.py @@ -62,6 +62,7 @@ def get_existing_form(item_id: str): } # Need to remove null fields response_json["name"] = response.name # Adding "name" since it's not present in the json + response_json["favorited"] = response.favorited except HTTPException: raise HTTPException(status_code=404, detail=f"Address with id: {item_id} not found") diff --git a/pscompose/api/routers/archives.py b/pscompose/api/routers/archives.py index ecf01b3..291b7e6 100644 --- a/pscompose/api/routers/archives.py +++ b/pscompose/api/routers/archives.py @@ -55,6 +55,7 @@ def get_existing_form(item_id: str): } # Need to remove null fields response_json["name"] = response.name # Adding "name" since it's not present in the json + response_json["favorited"] = response.favorited except HTTPException: raise HTTPException(status_code=404, detail=f"Address with id: {item_id} not found") diff --git a/pscompose/api/routers/contexts.py b/pscompose/api/routers/contexts.py index 75b76f7..1165698 100644 --- a/pscompose/api/routers/contexts.py +++ b/pscompose/api/routers/contexts.py @@ -53,6 +53,7 @@ def get_existing_form(item_id: str): } # Need to remove null fields response_json["name"] = response.name # Adding "name" since it's not present in the json + response_json["favorited"] = response.favorited except HTTPException: raise HTTPException(status_code=404, detail=f"Address with id: {item_id} not found") diff --git a/pscompose/api/routers/groups.py b/pscompose/api/routers/groups.py index 5371788..fca6a98 100644 --- a/pscompose/api/routers/groups.py +++ b/pscompose/api/routers/groups.py @@ -99,6 +99,7 @@ def get_existing_form(item_id: str): } # Need to remove null fields response_json["name"] = response.name # Adding "name" since it's not present in the json + response_json["favorited"] = response.favorited except HTTPException: raise HTTPException(status_code=404, detail=f"Group with id: {item_id} not found") diff --git a/pscompose/api/routers/home.py b/pscompose/api/routers/home.py new file mode 100644 index 0000000..7943ad2 --- /dev/null +++ b/pscompose/api/routers/home.py @@ -0,0 +1,23 @@ +from fastapi import APIRouter +from pscompose.backends.postgres import backend + +# Setup CRUD endpoints +router = APIRouter(tags=["home"]) + + +@router.get( + "/api/recently_edited", + summary="Return first 3 items of most recently edited", +) +def recently_edited(): + rows = backend.get_recently_edited(3) + return rows + + +@router.get( + "/api/favorites", + summary="Return first 3 items of favorites", +) +def favorites(): + rows = backend.get_favorites(3) + return rows diff --git a/pscompose/api/routers/schedules.py b/pscompose/api/routers/schedules.py index 7da043e..8180c0b 100644 --- a/pscompose/api/routers/schedules.py +++ b/pscompose/api/routers/schedules.py @@ -34,6 +34,7 @@ def get_existing_form(item_id: str): } # Need to remove null fields response_json["name"] = response.name # Adding "name" since it's not present in the json + response_json["favorited"] = response.favorited except HTTPException: raise HTTPException(status_code=404, detail=f"Address with id: {item_id} not found") diff --git a/pscompose/api/routers/tasks.py b/pscompose/api/routers/tasks.py index fd43ec3..e18dee6 100644 --- a/pscompose/api/routers/tasks.py +++ b/pscompose/api/routers/tasks.py @@ -55,6 +55,7 @@ def get_existing_form(item_id: str): } # Need to remove null fields response_json["name"] = response.name # Adding "name" since it's not present in the json + response_json["favorited"] = response.favorited except HTTPException: raise HTTPException(status_code=404, detail=f"Task with id: {item_id} not found") diff --git a/pscompose/api/routers/templates.py b/pscompose/api/routers/templates.py index 20ad91d..aa303e4 100644 --- a/pscompose/api/routers/templates.py +++ b/pscompose/api/routers/templates.py @@ -36,6 +36,7 @@ def get_existing_form(item_id: str): response = backend.get_datatype(datatype=DataTypes.TEMPLATE, item_id=item_id) response_json = response.json response_json["name"] = response.name # Adding "name" since it's not present in the json + response_json["favorited"] = response.favorited except HTTPException: raise HTTPException(status_code=404, detail=f"Address with id: {item_id} not found") diff --git a/pscompose/api/routers/tests.py b/pscompose/api/routers/tests.py index 76e3ac8..6350ee2 100644 --- a/pscompose/api/routers/tests.py +++ b/pscompose/api/routers/tests.py @@ -252,6 +252,7 @@ def get_existing_form(item_id: str): } # Need to remove null fields response_json["name"] = response.name # Adding "name" since it's not present in the json + response_json["favorited"] = response.favorited except HTTPException: raise HTTPException(status_code=404, detail=f"Test with id: {item_id} not found") diff --git a/pscompose/backends/postgres.py b/pscompose/backends/postgres.py index 8de5ab9..7284a3d 100644 --- a/pscompose/backends/postgres.py +++ b/pscompose/backends/postgres.py @@ -15,7 +15,9 @@ def __init__(self): DataTable.__table__.create(bind=engine, checkfirst=True) self.session = sessionmaker(bind=engine)() - def create_datatype(self, ref_set, datatype, json, name, created_by, last_edited_by): + def create_datatype( + self, ref_set, datatype, json, name, created_by, last_edited_by, favorited + ): try: new_type = DataTable( ref_set=ref_set, @@ -24,6 +26,7 @@ def create_datatype(self, ref_set, datatype, json, name, created_by, last_edited name=name, created_by=created_by, last_edited_by=last_edited_by, + favorited=favorited, # created_at = created_at, # last_edited_at = last_edited_at, # url = url @@ -61,6 +64,7 @@ def update_datatype(self, existing_result, updated_data): "ref_set": existing_result.ref_set, "last_edited_by": existing_result.last_edited_by, "last_edited_at": existing_result.last_edited_at, + "favorited": existing_result.favorited, } except Exception as e: self.session.rollback() @@ -179,5 +183,20 @@ def get_datatype(self, datatype, item_id): else: return result + def get_recently_edited(self, limit: int = 5): + query = self.session.query(DataTable).order_by(DataTable.created_at.desc()).limit(limit) + rows = query.all() + return [row for row in rows] + + def get_favorites(self, limit: int = 5): + query = ( + self.session.query(DataTable) + .filter_by(favorited=True) + .order_by(DataTable.created_at.desc()) + .limit(limit) + ) + rows = query.all() + return [row for row in rows] + backend = PostgresBackend() diff --git a/pscompose/form_schemas.py b/pscompose/form_schemas.py index 3e4f281..0706be4 100644 --- a/pscompose/form_schemas.py +++ b/pscompose/form_schemas.py @@ -89,12 +89,12 @@ { "type": "Control", "scope": "#/properties/lead-bind-address", - "customComponent": "input-text", + "customComponent": "input-text-autocomplete", }, { "type": "Control", "scope": "#/properties/pscheduler-address", - "customComponent": "input-text", + "customComponent": "input-text-autocomplete", }, { "type": "Control", diff --git a/pscompose/frontend/app/index.html b/pscompose/frontend/app/index.html index 29d2af5..fe56d14 100644 --- a/pscompose/frontend/app/index.html +++ b/pscompose/frontend/app/index.html @@ -42,8 +42,10 @@ + + @@ -246,7 +248,7 @@ icon: "locate", }, ], - addresses: { + address: { singular: "Address", plural: "Addresses", page_url: "/addresses/", @@ -254,7 +256,7 @@ list_endpoint: "http://localhost:8000/api/address/", icon: "hard-drive-download", }, - archives: { + archive: { singular: "Archive", plural: "Archives", page_url: "/archives/", @@ -262,7 +264,7 @@ list_endpoint: "http://localhost:8000/api/archive/", icon: "database", }, - contexts: { + context: { singular: "Context", plural: "Contexts", page_url: "/contexts/", @@ -270,7 +272,7 @@ list_endpoint: "http://localhost:8000/api/context/", icon: "file-question", }, - groups: { + group: { singular: "Group", plural: "Groups", page_url: "/groups/", @@ -278,7 +280,7 @@ list_endpoint: "http://localhost:8000/api/group/", icon: "group", }, - schedules: { + schedule: { singular: "Schedule", plural: "Schedules", page_url: "/schedules/", @@ -286,7 +288,7 @@ list_endpoint: "http://localhost:8000/api/schedule/", icon: "calendar-check", }, - tasks: { + task: { singular: "Task", plural: "Tasks", page_url: "/tasks/", @@ -294,7 +296,7 @@ list_endpoint: "http://localhost:8000/api/task/", icon: "clipboard-list", }, - templates: { + template: { singular: "Template", plural: "Templates", page_url: "/templates/", @@ -302,7 +304,7 @@ list_endpoint: "http://localhost:8000/api/template/", icon: "locate", }, - tests: { + test: { singular: "Test", plural: "Tests", page_url: "/tests/", @@ -313,14 +315,14 @@ }, }; psCompose.menu = { - addresses: psCompose.metadata["addresses"], - archives: psCompose.metadata["archives"], - contexts: psCompose.metadata["contexts"], - groups: psCompose.metadata["groups"], - schedules: psCompose.metadata["schedules"], - tasks: psCompose.metadata["tasks"], - templates: psCompose.metadata["templates"], - tests: psCompose.metadata["tests"], + address: psCompose.metadata["address"], + archive: psCompose.metadata["archive"], + context: psCompose.metadata["context"], + group: psCompose.metadata["group"], + schedule: psCompose.metadata["schedule"], + task: psCompose.metadata["task"], + template: psCompose.metadata["template"], + test: psCompose.metadata["test"], }; psCompose.router = { "/wizard/1/addresses/": psCompose.metadata["wizard"][0], @@ -333,22 +335,22 @@ "/wizard/8/template/": psCompose.metadata["wizard"][7], "/import/1/import_template/": psCompose.metadata["import"][0], "/import/2/import_template_saved/": psCompose.metadata["import"][1], - "/addresses/": psCompose.metadata["addresses"], - "/addresses/new/": psCompose.metadata["addresses"], - "/archives/": psCompose.metadata["archives"], - "/archives/new/": psCompose.metadata["archives"], - "/contexts/": psCompose.metadata["contexts"], - "/contexts/new/": psCompose.metadata["contexts"], - "/groups/": psCompose.metadata["groups"], - "/groups/new/": psCompose.metadata["groups"], - "/schedules/": psCompose.metadata["schedules"], - "/schedules/new/": psCompose.metadata["schedules"], - "/tasks/": psCompose.metadata["tasks"], - "/tasks/new/": psCompose.metadata["tasks"], - "/templates/": psCompose.metadata["templates"], - "/templates/new/": psCompose.metadata["templates"], - "/tests/": psCompose.metadata["tests"], - "/tests/new/": psCompose.metadata["tests"], + "/addresses/": psCompose.metadata["address"], + "/addresses/new/": psCompose.metadata["address"], + "/archives/": psCompose.metadata["archive"], + "/archives/new/": psCompose.metadata["archive"], + "/contexts/": psCompose.metadata["context"], + "/contexts/new/": psCompose.metadata["context"], + "/groups/": psCompose.metadata["group"], + "/groups/new/": psCompose.metadata["group"], + "/schedules/": psCompose.metadata["schedule"], + "/schedules/new/": psCompose.metadata["schedule"], + "/tasks/": psCompose.metadata["task"], + "/tasks/new/": psCompose.metadata["task"], + "/templates/": psCompose.metadata["template"], + "/templates/new/": psCompose.metadata["template"], + "/tests/": psCompose.metadata["test"], + "/tests/new/": psCompose.metadata["test"], }; // the actual URL router @@ -358,7 +360,7 @@ return "/pages/index.html"; } psCompose.activeRoute = psCompose.router[path]; - psCompose.activeMenuItem = path.split("/")[1]; + psCompose.activeMenuItem = psCompose.router[path].singular.toLowerCase(); psCompose.subsection = path.split("/")[2]; psCompose.pathParts = path.split("/"); diff --git a/pscompose/frontend/components/dropdown-single-select.js b/pscompose/frontend/components/dropdown-single-select.js index fda4fc1..d313302 100644 --- a/pscompose/frontend/components/dropdown-single-select.js +++ b/pscompose/frontend/components/dropdown-single-select.js @@ -102,6 +102,13 @@ export class SingleSelectDropdown extends FormControl { li.addEventListener("click", (e) => this.handleOptionClick(e)); this.optionsEl.appendChild(li); }); + document.addEventListener("click", (e) => this.closeDropdownOutside(e)); + } + + closeDropdownOutside(e) { + if (!this.contains(e.target)) { + this.optionsEl.classList.remove("open"); + } } render() { diff --git a/pscompose/frontend/components/input-checkbox-star.js b/pscompose/frontend/components/input-checkbox-star.js new file mode 100644 index 0000000..483a5a2 --- /dev/null +++ b/pscompose/frontend/components/input-checkbox-star.js @@ -0,0 +1,62 @@ +class InputCheckboxStar extends HTMLElement { + static get observedAttributes() { + return ["value", "disabled"]; + } + + get value() { + return this._value === true; + } + + set value(v) { + this._value = Boolean(v); + this.setAttribute("value", this._value); + } + + get disabled() { + return this.hasAttribute("disabled"); + } + + set disabled(v) { + v ? this.setAttribute("disabled", "") : this.removeAttribute("disabled"); + } + + constructor() { + super(); + } + + connectedCallback() { + this.render(); + lucide.createIcons(); + } + + attributeChangedCallback(name, oldValue, newValue) { + if (oldValue == newValue) return; + this.render(); + lucide.createIcons(); + } + + toggle() { + if (this.hasAttribute("disabled")) return; + + this.value = !this.value; + this.dispatchEvent(new Event("change", { bubbles: true })); + } + + attachEventListener() { + this.starEl.addEventListener("click", () => this.toggle()); + } + + render() { + const disabled = this.hasAttribute("disabled"); + + this.innerHTML = ``; + this.starEl = this.querySelector("button"); + this.attachEventListener(); + } +} + +customElements.define("input-checkbox-star", InputCheckboxStar); diff --git a/pscompose/frontend/components/input-checkbox.js b/pscompose/frontend/components/input-checkbox.js index 992fff8..48bceee 100644 --- a/pscompose/frontend/components/input-checkbox.js +++ b/pscompose/frontend/components/input-checkbox.js @@ -1,6 +1,6 @@ import { InputText } from "./input-text.js"; -class InputCheckbox extends InputText { +export class InputCheckbox extends InputText { render() { super.render(); this.inputEl.checked = this.value === "true" || this.value === true; diff --git a/pscompose/frontend/components/input-text-autocomplete.js b/pscompose/frontend/components/input-text-autocomplete.js new file mode 100644 index 0000000..6a904a9 --- /dev/null +++ b/pscompose/frontend/components/input-text-autocomplete.js @@ -0,0 +1,105 @@ +import { InputText } from "./input-text.js"; + +class InputTextAutocomplete extends InputText { + constructor() { + super(); + this.slotEl = ` +