From d4f7da2230e8aa97480806eb43ee504a91cfe440 Mon Sep 17 00:00:00 2001 From: codeEmpress1 Date: Tue, 6 May 2025 18:43:15 +0300 Subject: [PATCH 01/24] update nav --- .../js/src/store/components/Banner/Banner.tsx | 45 +-- .../Banner/__tests__/Banner.test.tsx | 286 +++++++++--------- templates/partial/_navigation.html | 103 ++----- 3 files changed, 168 insertions(+), 266 deletions(-) diff --git a/static/js/src/store/components/Banner/Banner.tsx b/static/js/src/store/components/Banner/Banner.tsx index 6e569061..96098311 100644 --- a/static/js/src/store/components/Banner/Banner.tsx +++ b/static/js/src/store/components/Banner/Banner.tsx @@ -1,47 +1,16 @@ -import { RefObject } from "react"; -import { useSearchParams } from "react-router-dom"; import { Strip, Row, Col } from "@canonical/react-components"; -type Props = { - searchRef: RefObject; - searchSummaryRef: RefObject; -}; - -function Banner({ searchRef }: Props) { - const [searchParams, setSearchParams] = useSearchParams(); +function Banner() { return ( - -

The Rocks Collection

-
- - - - -
+ +

+ The Ubuntu-based +
+ container images store +

diff --git a/static/js/src/store/components/Banner/__tests__/Banner.test.tsx b/static/js/src/store/components/Banner/__tests__/Banner.test.tsx index 46140131..372abdd0 100644 --- a/static/js/src/store/components/Banner/__tests__/Banner.test.tsx +++ b/static/js/src/store/components/Banner/__tests__/Banner.test.tsx @@ -1,143 +1,143 @@ -import React from "react"; -import { render, screen, fireEvent } from "@testing-library/react"; -import "@testing-library/jest-dom"; -import { useSearchParams } from "react-router-dom"; -import Banner from "../Banner"; - -jest.mock("react-router-dom", () => ({ - useSearchParams: jest.fn(), -})); - -window.HTMLElement.prototype.scrollIntoView = jest.fn(); - -describe("Banner Component", () => { - let mockSetSearchParams: jest.Mock; - let mockSearchRef: React.RefObject; - let mockSearchSummaryRef: React.RefObject; - - beforeEach(() => { - mockSetSearchParams = jest.fn(); - (useSearchParams as jest.Mock).mockReturnValue([ - new URLSearchParams(), - mockSetSearchParams, - ]); - - mockSearchRef = { - current: document.createElement("input"), - }; - - mockSearchSummaryRef = { - current: document.createElement("div"), - }; - }); - - test("should render the Banner component", () => { - render( - - ); - - expect( - screen.getByRole("heading", { name: /The Rocks Collection/i }) - ).toBeInTheDocument(); - - expect(screen.getByPlaceholderText("Search Rocks")).toBeInTheDocument(); - - expect(screen.getByRole("button", { name: /Search/i })).toBeInTheDocument(); - expect(screen.getByRole("button", { name: /Close/i })).toBeInTheDocument(); - }); - - test("should update search params and scroll on form submission", () => { - render( - - ); - - if (mockSearchRef.current) { - mockSearchRef.current.value = "kubernetes"; - } - - fireEvent.submit(screen.getByRole("button", { name: /Search/i })); - - expect(mockSetSearchParams).toHaveBeenCalledWith( - new URLSearchParams({ q: "kubernetes" }) - ); - - expect(mockSearchSummaryRef.current?.scrollIntoView).toHaveBeenCalledWith({ - behavior: "smooth", - }); - }); - - test("should clear search params on reset button click", () => { - render( - - ); - - fireEvent.click(screen.getByRole("button", { name: /Close/i })); - - expect(mockSetSearchParams).toHaveBeenCalledWith(new URLSearchParams()); - }); - - test("should not update search params when input is empty", () => { - render( - - ); - - if (mockSearchRef.current) { - mockSearchRef.current.value = ""; - } - - fireEvent.submit(screen.getByRole("button", { name: /Search/i })); - - expect(mockSetSearchParams).not.toHaveBeenCalled(); - }); - - test("should preserve query parameter in input field", () => { - const searchParams = new URLSearchParams({ q: "test" }); - (useSearchParams as jest.Mock).mockReturnValue([ - searchParams, - mockSetSearchParams, - ]); - - render( - - ); - - const searchInput = screen.getByPlaceholderText( - "Search Rocks" - ) as HTMLInputElement; - expect(searchInput.value).toBe("test"); - }); - - test("should call scrollIntoView on form submission", () => { - render( - - ); - - if (mockSearchSummaryRef.current) { - mockSearchSummaryRef.current.scrollIntoView = jest.fn(); - } - - fireEvent.submit(screen.getByRole("button", { name: /Search/i })); - - expect(mockSearchSummaryRef.current?.scrollIntoView).toHaveBeenCalledWith({ - behavior: "smooth", - }); - }); -}); +// import React from "react"; +// import { render, screen, fireEvent } from "@testing-library/react"; +// import "@testing-library/jest-dom"; +// import { useSearchParams } from "react-router-dom"; +// import Banner from "../Banner"; + +// jest.mock("react-router-dom", () => ({ +// useSearchParams: jest.fn(), +// })); + +// window.HTMLElement.prototype.scrollIntoView = jest.fn(); + +// describe("Banner Component", () => { +// let mockSetSearchParams: jest.Mock; +// let mockSearchRef: React.RefObject; +// let mockSearchSummaryRef: React.RefObject; + +// beforeEach(() => { +// mockSetSearchParams = jest.fn(); +// (useSearchParams as jest.Mock).mockReturnValue([ +// new URLSearchParams(), +// mockSetSearchParams, +// ]); + +// mockSearchRef = { +// current: document.createElement("input"), +// }; + +// mockSearchSummaryRef = { +// current: document.createElement("div"), +// }; +// }); + +// test("should render the Banner component", () => { +// render( +// +// ); + +// expect( +// screen.getByRole("heading", { name: /The Rocks Collection/i }) +// ).toBeInTheDocument(); + +// expect(screen.getByPlaceholderText("Search Rocks")).toBeInTheDocument(); + +// expect(screen.getByRole("button", { name: /Search/i })).toBeInTheDocument(); +// expect(screen.getByRole("button", { name: /Close/i })).toBeInTheDocument(); +// }); + +// test("should update search params and scroll on form submission", () => { +// render( +// +// ); + +// if (mockSearchRef.current) { +// mockSearchRef.current.value = "kubernetes"; +// } + +// fireEvent.submit(screen.getByRole("button", { name: /Search/i })); + +// expect(mockSetSearchParams).toHaveBeenCalledWith( +// new URLSearchParams({ q: "kubernetes" }) +// ); + +// expect(mockSearchSummaryRef.current?.scrollIntoView).toHaveBeenCalledWith({ +// behavior: "smooth", +// }); +// }); + +// test("should clear search params on reset button click", () => { +// render( +// +// ); + +// fireEvent.click(screen.getByRole("button", { name: /Close/i })); + +// expect(mockSetSearchParams).toHaveBeenCalledWith(new URLSearchParams()); +// }); + +// test("should not update search params when input is empty", () => { +// render( +// +// ); + +// if (mockSearchRef.current) { +// mockSearchRef.current.value = ""; +// } + +// fireEvent.submit(screen.getByRole("button", { name: /Search/i })); + +// expect(mockSetSearchParams).not.toHaveBeenCalled(); +// }); + +// test("should preserve query parameter in input field", () => { +// const searchParams = new URLSearchParams({ q: "test" }); +// (useSearchParams as jest.Mock).mockReturnValue([ +// searchParams, +// mockSetSearchParams, +// ]); + +// render( +// +// ); + +// const searchInput = screen.getByPlaceholderText( +// "Search Rocks" +// ) as HTMLInputElement; +// expect(searchInput.value).toBe("test"); +// }); + +// test("should call scrollIntoView on form submission", () => { +// render( +// +// ); + +// if (mockSearchSummaryRef.current) { +// mockSearchSummaryRef.current.scrollIntoView = jest.fn(); +// } + +// fireEvent.submit(screen.getByRole("button", { name: /Search/i })); + +// expect(mockSearchSummaryRef.current?.scrollIntoView).toHaveBeenCalledWith({ +// behavior: "smooth", +// }); +// }); +// }); diff --git a/templates/partial/_navigation.html b/templates/partial/_navigation.html index c1da58c9..88aa03cf 100644 --- a/templates/partial/_navigation.html +++ b/templates/partial/_navigation.html @@ -1,12 +1,12 @@ - From f5abd8e0fd708a1faf9d887df82281b270381b7b Mon Sep 17 00:00:00 2001 From: codeEmpress1 Date: Tue, 17 Jun 2025 15:06:50 +0300 Subject: [PATCH 02/24] remove search functionality --- static/js/src/search/App.tsx | 104 ------------------ .../store/components/Packages/Packages.tsx | 45 ++------ templates/all-search.html | 5 - templates/partial/_navigation.html | 2 +- webapp/app.py | 2 - webapp/search/views.py | 29 ----- webpack.config.entry.js | 1 - 7 files changed, 8 insertions(+), 180 deletions(-) delete mode 100644 static/js/src/search/App.tsx delete mode 100644 templates/all-search.html delete mode 100644 webapp/search/views.py diff --git a/static/js/src/search/App.tsx b/static/js/src/search/App.tsx deleted file mode 100644 index ca872207..00000000 --- a/static/js/src/search/App.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { Col, Row } from "@canonical/react-components"; -import { CharmCard, LoadingCard } from "@canonical/store-components"; -import { useEffect, useState } from "react"; -import { createRoot } from "react-dom/client"; -import { Package, Publisher } from "../store/types"; - -function App() { - const search = new URLSearchParams(window.location.search).get("q"); - const [loading, setLoading] = useState(true); - - const [term, setTerm] = useState(search || ""); - - const [results, setResults] = useState({ - rocks: [], - }); - - useEffect(() => { - if (!search) return; - fetch(`/all-search.json?q=${search}&limit=4`) - .then((response) => response.json()) - .then((data) => { - setResults(data); - setLoading(false); - }); - }, []); - const { rocks } = results; - - return ( - <> -
-
- {search ? ( -

Search results for "{search}"

- ) : ( -

Search Rocks

- )} -
- setTerm(e.target.value)} - type="search" - className="p-search-box__input" - name="q" - placeholder="Search Rocks" - required - /> - - -
-
-
- {search && ( -
-
-

- Rocks › -

- - {loading ? ( - [...Array(4)].map((_, i) => ( - - - - )) - ) : rocks.length ? ( - <> -

- Showing the top {rocks.length} results for "{search}" -

- {rocks.map( - (rock: { - package: Package; - publisher: Publisher; - id: string; - }) => ( - -

{rock.package.name}

- - - ) - )} - - ) : ( -

No rocks matching this search

- )} -
-
-
- )} - - ); -} - -const container = document.getElementById("main-content"); -const root = createRoot(container as HTMLElement); -root.render(); diff --git a/static/js/src/store/components/Packages/Packages.tsx b/static/js/src/store/components/Packages/Packages.tsx index 31978c35..e89e8627 100644 --- a/static/js/src/store/components/Packages/Packages.tsx +++ b/static/js/src/store/components/Packages/Packages.tsx @@ -1,6 +1,5 @@ import { useEffect, useRef } from "react"; import { useQuery } from "react-query"; -import { useSearchParams } from "react-router-dom"; import { Strip, Row, @@ -33,52 +32,24 @@ function Packages() { }; }; - const [searchParams, setSearchParams] = useSearchParams(); - const currentPage = searchParams.get("page") || "1"; + const currentPage = "1"; const { data, status, refetch, isFetching } = useQuery("data", getData); - const searchRef = useRef(null); - const searchSummaryRef = useRef(null); - useEffect(() => { - refetch(); - }, [searchParams]); const firstResultNumber = (parseInt(currentPage) - 1) * ITEMS_PER_PAGE + 1; const lastResultNumber = (parseInt(currentPage) - 1) * ITEMS_PER_PAGE + data?.packages.length; return ( <> - + {data?.packages && data?.packages.length > 0 && ( -
- {searchParams.get("q") ? ( -

- Showing {currentPage === "1" ? "1" : firstResultNumber} to{" "} - {lastResultNumber} of {data?.total_items} results for{" "} - "{searchParams.get("q")}".{" "} - -

- ) : ( -

- Showing {currentPage === "1" ? "1" : firstResultNumber} to{" "} - {lastResultNumber} of {data?.total_items} items -

- )} +
+

+ Showing {currentPage === "1" ? "1" : firstResultNumber} to{" "} + {lastResultNumber} of {data?.total_items} items +

)} @@ -118,8 +89,6 @@ function Packages() { itemsPerPage={ITEMS_PER_PAGE} totalItems={data.total_items} paginate={(pageNumber) => { - searchParams.set("page", pageNumber.toString()); - setSearchParams(searchParams); }} currentPage={parseInt(currentPage)} centered diff --git a/templates/all-search.html b/templates/all-search.html deleted file mode 100644 index 5b49173b..00000000 --- a/templates/all-search.html +++ /dev/null @@ -1,5 +0,0 @@ -{% extends 'base_layout.html' %} - -{% block content %} - -{% endblock content %} diff --git a/templates/partial/_navigation.html b/templates/partial/_navigation.html index 88aa03cf..8781126e 100644 --- a/templates/partial/_navigation.html +++ b/templates/partial/_navigation.html @@ -44,6 +44,6 @@
-
+
diff --git a/webapp/app.py b/webapp/app.py index f4038d56..bd8b5f03 100644 --- a/webapp/app.py +++ b/webapp/app.py @@ -6,7 +6,6 @@ from webapp.config import APP_NAME from webapp.handlers import set_handlers from webapp.store.views import store -from webapp.search.views import search from webapp.helpers import markdown_to_html from canonicalwebteam.flask_base.app import FlaskBase from webapp.packages.store_packages import store_packages @@ -52,7 +51,6 @@ def replace_with_link(match): app.register_blueprint(store_packages) app.register_blueprint(store) -app.register_blueprint(search) app.jinja_env.filters["markdown"] = markdown_to_html diff --git a/webapp/search/views.py b/webapp/search/views.py deleted file mode 100644 index 662b9886..00000000 --- a/webapp/search/views.py +++ /dev/null @@ -1,29 +0,0 @@ -from flask import Blueprint, json, request, render_template - -from webapp.packages.logic import parse_package_for_card - - -search = Blueprint( - "search", __name__, template_folder="/templates", static_folder="/static" -) - - -@search.route("/all-search") -def all_search(): - return render_template("all-search.html") - - -@search.route("/all-search.json") -def all_search_json(): - params = request.args - term = params.get("q") - limit = int(params.get("limit", 5)) - - with open("webapp/rocks.json", "r") as rocks: - rocks = json.load(rocks) - rocks = [ - parse_package_for_card(rock) - for rock in rocks - if term in rock["display_name"] - ][:limit] - return {"rocks": rocks} diff --git a/webpack.config.entry.js b/webpack.config.entry.js index 6429c64f..997585d3 100644 --- a/webpack.config.entry.js +++ b/webpack.config.entry.js @@ -5,5 +5,4 @@ module.exports = { details: "./static/js/src/public/details/index.ts", details_overview: "./static/js/src/public/details/overview/index.js", store: "./static/js/src/store/index.tsx", - search: "./static/js/src/search/App.tsx", }; From b3b8f9046d9a31c8408bcd6e5067ee73d3122ac5 Mon Sep 17 00:00:00 2001 From: codeEmpress1 Date: Tue, 17 Jun 2025 15:24:37 +0300 Subject: [PATCH 03/24] update banner test --- .../Banner/__tests__/Banner.test.tsx | 167 +++--------------- 1 file changed, 24 insertions(+), 143 deletions(-) diff --git a/static/js/src/store/components/Banner/__tests__/Banner.test.tsx b/static/js/src/store/components/Banner/__tests__/Banner.test.tsx index 372abdd0..ce46e928 100644 --- a/static/js/src/store/components/Banner/__tests__/Banner.test.tsx +++ b/static/js/src/store/components/Banner/__tests__/Banner.test.tsx @@ -1,143 +1,24 @@ -// import React from "react"; -// import { render, screen, fireEvent } from "@testing-library/react"; -// import "@testing-library/jest-dom"; -// import { useSearchParams } from "react-router-dom"; -// import Banner from "../Banner"; - -// jest.mock("react-router-dom", () => ({ -// useSearchParams: jest.fn(), -// })); - -// window.HTMLElement.prototype.scrollIntoView = jest.fn(); - -// describe("Banner Component", () => { -// let mockSetSearchParams: jest.Mock; -// let mockSearchRef: React.RefObject; -// let mockSearchSummaryRef: React.RefObject; - -// beforeEach(() => { -// mockSetSearchParams = jest.fn(); -// (useSearchParams as jest.Mock).mockReturnValue([ -// new URLSearchParams(), -// mockSetSearchParams, -// ]); - -// mockSearchRef = { -// current: document.createElement("input"), -// }; - -// mockSearchSummaryRef = { -// current: document.createElement("div"), -// }; -// }); - -// test("should render the Banner component", () => { -// render( -// -// ); - -// expect( -// screen.getByRole("heading", { name: /The Rocks Collection/i }) -// ).toBeInTheDocument(); - -// expect(screen.getByPlaceholderText("Search Rocks")).toBeInTheDocument(); - -// expect(screen.getByRole("button", { name: /Search/i })).toBeInTheDocument(); -// expect(screen.getByRole("button", { name: /Close/i })).toBeInTheDocument(); -// }); - -// test("should update search params and scroll on form submission", () => { -// render( -// -// ); - -// if (mockSearchRef.current) { -// mockSearchRef.current.value = "kubernetes"; -// } - -// fireEvent.submit(screen.getByRole("button", { name: /Search/i })); - -// expect(mockSetSearchParams).toHaveBeenCalledWith( -// new URLSearchParams({ q: "kubernetes" }) -// ); - -// expect(mockSearchSummaryRef.current?.scrollIntoView).toHaveBeenCalledWith({ -// behavior: "smooth", -// }); -// }); - -// test("should clear search params on reset button click", () => { -// render( -// -// ); - -// fireEvent.click(screen.getByRole("button", { name: /Close/i })); - -// expect(mockSetSearchParams).toHaveBeenCalledWith(new URLSearchParams()); -// }); - -// test("should not update search params when input is empty", () => { -// render( -// -// ); - -// if (mockSearchRef.current) { -// mockSearchRef.current.value = ""; -// } - -// fireEvent.submit(screen.getByRole("button", { name: /Search/i })); - -// expect(mockSetSearchParams).not.toHaveBeenCalled(); -// }); - -// test("should preserve query parameter in input field", () => { -// const searchParams = new URLSearchParams({ q: "test" }); -// (useSearchParams as jest.Mock).mockReturnValue([ -// searchParams, -// mockSetSearchParams, -// ]); - -// render( -// -// ); - -// const searchInput = screen.getByPlaceholderText( -// "Search Rocks" -// ) as HTMLInputElement; -// expect(searchInput.value).toBe("test"); -// }); - -// test("should call scrollIntoView on form submission", () => { -// render( -// -// ); - -// if (mockSearchSummaryRef.current) { -// mockSearchSummaryRef.current.scrollIntoView = jest.fn(); -// } - -// fireEvent.submit(screen.getByRole("button", { name: /Search/i })); - -// expect(mockSearchSummaryRef.current?.scrollIntoView).toHaveBeenCalledWith({ -// behavior: "smooth", -// }); -// }); -// }); +import React from "react"; +import { render, screen, fireEvent } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import { useSearchParams } from "react-router-dom"; +import Banner from "../Banner"; + +jest.mock("react-router-dom", () => ({ + useSearchParams: jest.fn(), +})); + +window.HTMLElement.prototype.scrollIntoView = jest.fn(); + +describe("Banner Component", () => { + test("should render the Banner component", () => { + render( + + ); + + expect( + screen.getByRole("heading", { name: /The Ubuntu-based container images store/i }) + ).toBeInTheDocument(); + }); + +}); From 97f0edad15a1bb3f057c06f114ebb0037df606b3 Mon Sep 17 00:00:00 2001 From: codeEmpress1 Date: Tue, 17 Jun 2025 16:21:51 +0300 Subject: [PATCH 04/24] fix lints --- static/js/src/store/components/Banner/Banner.tsx | 2 +- .../components/Banner/__tests__/Banner.test.tsx | 11 +++++------ .../js/src/store/components/Packages/Packages.tsx | 14 +++----------- 3 files changed, 9 insertions(+), 18 deletions(-) diff --git a/static/js/src/store/components/Banner/Banner.tsx b/static/js/src/store/components/Banner/Banner.tsx index 2677214d..967cfd5a 100644 --- a/static/js/src/store/components/Banner/Banner.tsx +++ b/static/js/src/store/components/Banner/Banner.tsx @@ -7,7 +7,7 @@ function Banner() {

The Ubuntu-based -
+
container images store

diff --git a/static/js/src/store/components/Banner/__tests__/Banner.test.tsx b/static/js/src/store/components/Banner/__tests__/Banner.test.tsx index 69a29cff..21b2c9be 100644 --- a/static/js/src/store/components/Banner/__tests__/Banner.test.tsx +++ b/static/js/src/store/components/Banner/__tests__/Banner.test.tsx @@ -1,7 +1,6 @@ import React from "react"; -import { render, screen, fireEvent, waitFor } from "@testing-library/react"; +import { render, screen } from "@testing-library/react"; import "@testing-library/jest-dom"; -import { useSearchParams } from "react-router-dom"; import Banner from "../Banner"; jest.mock("react-router-dom", () => ({ @@ -10,11 +9,11 @@ jest.mock("react-router-dom", () => ({ describe("Banner Component", () => { test("should render the Banner component", () => { - render( - - ); + render(); expect( - screen.getByRole("heading", { name: /The Ubuntu-based container images store/i }) + screen.getByRole("heading", { + name: /The Ubuntu-based container images store/i, + }) ).toBeInTheDocument(); }); }); diff --git a/static/js/src/store/components/Packages/Packages.tsx b/static/js/src/store/components/Packages/Packages.tsx index e89e8627..453f163d 100644 --- a/static/js/src/store/components/Packages/Packages.tsx +++ b/static/js/src/store/components/Packages/Packages.tsx @@ -1,12 +1,5 @@ -import { useEffect, useRef } from "react"; import { useQuery } from "react-query"; -import { - Strip, - Row, - Col, - Pagination, - Button, -} from "@canonical/react-components"; +import { Strip, Row, Col, Pagination } from "@canonical/react-components"; import { CharmCard, LoadingCard } from "@canonical/store-components"; import Banner from "../Banner"; @@ -33,7 +26,7 @@ function Packages() { }; const currentPage = "1"; - const { data, status, refetch, isFetching } = useQuery("data", getData); + const { data, status, isFetching } = useQuery("data", getData); const firstResultNumber = (parseInt(currentPage) - 1) * ITEMS_PER_PAGE + 1; const lastResultNumber = @@ -88,8 +81,7 @@ function Packages() { { - }} + paginate={() => {}} currentPage={parseInt(currentPage)} centered scrollToTop From 1b98b375c995a1501c42e04c77ac1bcec97a204d Mon Sep 17 00:00:00 2001 From: codeEmpress1 Date: Tue, 17 Jun 2025 17:33:29 +0300 Subject: [PATCH 05/24] fix broken pagination --- .../store/components/Packages/Packages.tsx | 32 +++++++++++++------ templates/partial/_navigation.html | 1 - 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/static/js/src/store/components/Packages/Packages.tsx b/static/js/src/store/components/Packages/Packages.tsx index 453f163d..8b515cbe 100644 --- a/static/js/src/store/components/Packages/Packages.tsx +++ b/static/js/src/store/components/Packages/Packages.tsx @@ -1,4 +1,6 @@ +import { useEffect } from "react"; import { useQuery } from "react-query"; +import { useSearchParams } from "react-router-dom"; import { Strip, Row, Col, Pagination } from "@canonical/react-components"; import { CharmCard, LoadingCard } from "@canonical/store-components"; @@ -12,12 +14,11 @@ function Packages() { const response = await fetch(`/store.json`); const data = await response.json(); - const packagesWithId = data.packages.map((item: string[]) => { - return { - ...item, - id: crypto.randomUUID(), - }; - }); + const packagesWithId = data.packages.map((item: string[]) => ({ + ...item, + id: crypto.randomUUID(), + })); + return { total_items: data.total_items, total_pages: data.total_pages, @@ -25,12 +26,19 @@ function Packages() { }; }; - const currentPage = "1"; - const { data, status, isFetching } = useQuery("data", getData); + const [searchParams, setSearchParams] = useSearchParams(); + const currentPage = searchParams.get("page") || "1"; + + const { data, status, refetch, isFetching } = useQuery("data", getData); + + useEffect(() => { + refetch(); + }, [searchParams]); const firstResultNumber = (parseInt(currentPage) - 1) * ITEMS_PER_PAGE + 1; const lastResultNumber = - (parseInt(currentPage) - 1) * ITEMS_PER_PAGE + data?.packages.length; + (parseInt(currentPage) - 1) * ITEMS_PER_PAGE + (data?.packages.length || 0); + return ( <> @@ -45,6 +53,7 @@ function Packages() {

)} + {isFetching && [...Array(ITEMS_PER_PAGE)].map((_item, index) => ( @@ -81,7 +90,10 @@ function Packages() { {}} + paginate={(pageNumber) => { + searchParams.set("page", pageNumber.toString()); + setSearchParams(searchParams); + }} currentPage={parseInt(currentPage)} centered scrollToTop diff --git a/templates/partial/_navigation.html b/templates/partial/_navigation.html index 8781126e..ea4be0d2 100644 --- a/templates/partial/_navigation.html +++ b/templates/partial/_navigation.html @@ -46,4 +46,3 @@
- From bff08fdbe7e8d5dfe592661c068de0a2497150c2 Mon Sep 17 00:00:00 2001 From: codeEmpress1 Date: Thu, 19 Jun 2025 10:51:49 +0300 Subject: [PATCH 06/24] feat:consume rocks find and info APIs --- webapp/store/logic.py | 286 ++++++++++++++++++++++++++++++++++++++++-- webapp/store/views.py | 42 +++---- 2 files changed, 293 insertions(+), 35 deletions(-) diff --git a/webapp/store/logic.py b/webapp/store/logic.py index b033c894..5d5c4f3e 100644 --- a/webapp/store/logic.py +++ b/webapp/store/logic.py @@ -1,17 +1,67 @@ -import datetime import humanize +from typing import List, Dict, TypedDict, Any, Union +import datetime from dateutil import parser -from webapp.helpers import get_yaml_loader -yaml = get_yaml_loader() +from canonicalwebteam.store_api.devicegw import DeviceGW -PLATFORMS = { - "ubuntu": "Ubuntu", - "centos": "CentOS", -} +from webapp.helpers import get_yaml_loader ARCHITECTURES = ["amd64", "arm64", "ppc64el", "riscv64", "s390x"] +FIND_FIELDS = [ + "contact", + "description", + "license", + "summary", + "title", + "website", + "publisher", + "categories", + "links", +] + +DETAILS_FIELDS = [ + "categories", + "contact", + "description", + "license", + "links", + "media", + "private", + "publisher", + "summary", + "title", + "website", + "created-at", + "download", + "version", + "revision", + "channel-map", +] + +yaml = get_yaml_loader() +device_gw = DeviceGW("rock", staging=True) + +Packages = TypedDict( + "Packages", + { + "packages": List[ + Dict[ + str, + Union[Dict[str, Union[str, List[str]]], List[Dict[str, str]]], + ] + ] + }, +) +Package = TypedDict( + "Package", + { + "package": Dict[ + str, Union[Dict[str, str], List[str], List[Dict[str, str]]] + ] + }, +) def convert_date(date_to_convert): """ @@ -32,6 +82,49 @@ def convert_date(date_to_convert): else: return date_parsed.strftime("%d %b %Y") +def format_relative_date(date_str: str) -> str: + """ + Converts an ISO 8601 date string to a human-readable relative format. + + Examples: + - "today" + - "yesterday" + - "last 3 days" + - "last 2 weeks" + - "last 1 month" + - "25 Apr 2025" (for dates > ~3 months ago) + + Args: + date_str (str): ISO 8601 datetime string (with timezone) + + Returns: + str: Human-readable relative date + """ + try: + given_date = datetime.datetime.fromisoformat(date_str) + now = datetime.datetime.now(datetime.timezone.utc) + delta = now - given_date + + if delta.days < 0: + return "in the future" + elif delta.days == 0: + return "today" + elif delta.days == 1: + return "yesterday" + elif delta.days < 7: + return f"{delta.days} days ago" + elif delta.days < 30: + weeks = delta.days // 7 + return f"{weeks} week{'s' if weeks > 1 else ''} ago" + elif delta.days < 90: + months = delta.days // 30 + return f"{months} month{'s' if months > 1 else ''} ago" + else: + return given_date.strftime("%d %b %Y") + + except Exception as e: + return f"Invalid date: {e}" + def get_icons(package): media = package["result"]["media"] @@ -51,3 +144,182 @@ def format_slug(slug): .replace("And", "and") .replace("Iot", "IoT") ) + +def get_icon(media): + icons = [m["url"] for m in media if m["type"] == "icon"] + if len(icons) > 0: + return icons[0] + return "" + +def parse_package_for_card( + package: Dict[str, Any], +) -> Package: + """ + Parses a package and returns the formatted package + based on the given card schema. + + :param: package (Dict[str, Any]): The package to be parsed. + :returns: a dictionary containing the formatted package. + + note: + - This function has to be refactored to be more generic, + so we won't have to check for the package type before parsing. + + """ + + resp = { + "package": { + "description": "", + "summary": "", + "display_name": "", + "icon_url": "", + "name": "", + "platforms": [], + "website": "", + "contact": "", + }, + "publisher": {"display_name": "", "name": "", "validation": ""}, + "ratings": {"value": "0", "count": "0"}, + } + + metadata = package.get("metadata", {}) + publisher = metadata.get("publisher", {}) + + resp["package"]["name"] = package.get("name", "") + resp["package"]["description"] = ( + metadata["summary"] or metadata["description"] + ) + resp["package"]["display_name"] = format_slug(metadata.get("title", "")) + resp["package"]["icon_url"] = get_icon(metadata.get("links", {}).get("media", [])) + resp["package"]["website"] = metadata.get("website", "") + resp["package"]["contact"] = metadata.get("contact", "") + + resp["publisher"]["display_name"] = publisher.get("display-name", "") + resp["publisher"]["name"] = publisher.get("username", "") + resp["publisher"]["validation"] = publisher.get("validation", "") + + return resp + +def paginate(packages: List[Packages], page: int, size: int) -> List[Packages]: + """ + Paginates the list of packages safely based on page and size. + + Returns only the correct slice of items for the given page. + """ + total_items = len(packages) + total_pages = (total_items + size - 1) // size # ceiling division + + if page > total_pages: + page = total_pages + if page < 1: + page = 1 + + start = (page - 1) * size + end = min(start + size, total_items) + + return packages[start:end] + +def parse_rock_details(rock): + parsed_rock = { + "display_name": "", + "name": rock.get("name", ""), + "description": rock["metadata"].get("description", ""), + "summary": rock["metadata"].get("summary", ""), + "icon_url": "", + "metadata": { + "license": rock["metadata"].get("license", ""), + "status": "chisel" if "chisel" in rock["name"] else "full", + "links": rock["metadata"].get("links", {}), + "private": rock["metadata"].get("private", False), + "rock_details": { + "repo_url": "", + "docs_url": "", + "SBOM_url": "", + }, + "upstream_details": {}, + "related_rocks": [], + "downstream_artifacts": {}, + + }, + "categories": rock["metadata"].get("categories", []), + "publisher": { + "name": format_slug(rock["metadata"]["publisher"].get("display-name", "")), + "username": rock["metadata"]["publisher"].get("username", ""), + "validation": rock["metadata"]["publisher"].get("validation", ""), + }, + "channels": [], + } + + parsed_rock["display_name"] = format_slug(rock.get("name", "")) + parsed_rock["icon_url"] = get_icon(rock["metadata"].get("media", [])) + parsed_rock["license"] = rock["metadata"].get("license", "") + + for channel in rock.get("channel-map", []): + channel_data = channel.get("channel", {}) + revision_data = channel.get("revision", {}) + v = revision_data.get("version", "").split(".") + if len(v) < 3: + v.append("0") + normalized_version = ".".join(v) + parsed_channel = { + "workload_version": normalized_version, + "risk": channel_data.get("risk", ""), + "last_updated": format_relative_date(channel_data.get("released-at", "")), + "released_at": convert_date(channel_data.get("released-at", "")), + "revision": revision_data["revision"], + "version": revision_data.get("version", ""), + "track": channel_data.get("track", ""), + } + parsed_rock["channels"].append(parsed_channel) + parsed_rock["latest_channel"] = max( + parsed_rock["channels"], + key=lambda x: datetime.datetime.strptime(x["released_at"], "%d %b %Y"), + ) + + return parsed_rock + +def get_rocks( + size: int = 10, + query_params: Dict[str, Any] = {}, +) -> List[Dict[str, Any]]: + """ + Retrieves a list of packages from the store based on the specified + parameters.The returned packages are paginated and parsed using the + card schema. + + """ + + rocks2 = device_gw.find("%", fields=FIND_FIELDS).get("results", []) + + total_items = len(rocks2) + total_pages = (total_items + size - 1) // size + page = int(query_params.get("page", 1)) + rocks_per_page = paginate(rocks2, page, size) + parsed_rocks = [] + + for rock in rocks_per_page: + parsed_rocks.append(parse_package_for_card(rock)) + return { + "packages": parsed_rocks, + "total_pages": total_pages, + "total_items": total_items, + } + +def get_rock( + entity_name: str, +) -> Dict[str, Any]: + """ + Retrieves a specific rock package by its name. + + :param: entity_name (str): The name of the rock package to retrieve. + :returns: a dictionary containing the rock package details. + + note: + - This function has to be refactored to be more generic, + so we won't have to check for the package type before parsing. + """ + rock = device_gw.get_item_details(entity_name, fields=DETAILS_FIELDS) + if not rock: + return {} + + return rock diff --git a/webapp/store/views.py b/webapp/store/views.py index 3a085d7d..d70266cd 100644 --- a/webapp/store/views.py +++ b/webapp/store/views.py @@ -1,46 +1,32 @@ import json -from flask import Blueprint -from flask import render_template, make_response +from flask import ( + Blueprint, + request, + make_response, + render_template, +) from webapp.config import DETAILS_VIEW_REGEX +from webapp.store.logic import get_rocks, get_rock, parse_rock_details store = Blueprint( "store", __name__, template_folder="/templates", static_folder="/static" ) - -def get_package(entity_name): - - with open("webapp/rocks.json") as f: - rocks = json.load(f) - for rock in rocks: - if rock["display_name"] == entity_name: - return rock - return rock - +@store.route("/store.json") +def get_store_packages(): + args = dict(request.args) + res = make_response(get_rocks(12, args)) + return res @store.route('/') def details_overview(entity_name): - package = get_package(entity_name) - package["display_name"] = entity_name.capitalize() - package["publisher"]["name"] = package["publisher"]["name"].capitalize() - package["metadata"] = { - "name": entity_name, - "base": "bare", - "build-base": "ubuntu@22.04", - "version": "0.1", - "summary": f"Rocked {entity_name}", - "description": f"Description for {entity_name}.", - "platforms": ["amd64"], - "result": { - "license": "Apache-2.0", - }, - } + rock = parse_rock_details(get_rock(entity_name)) context = { - "package": package, + "package": rock, "navigation": None, "last_update": None, "forum_url": None, From 302820b0c9d6584a7b02bdb9a91b010f3fcc06de Mon Sep 17 00:00:00 2001 From: codeEmpress1 Date: Thu, 19 Jun 2025 10:52:20 +0300 Subject: [PATCH 07/24] remove mock data --- webapp/rocks.json | 1186 --------------------------------------------- 1 file changed, 1186 deletions(-) delete mode 100644 webapp/rocks.json diff --git a/webapp/rocks.json b/webapp/rocks.json deleted file mode 100644 index 5f106785..00000000 --- a/webapp/rocks.json +++ /dev/null @@ -1,1186 +0,0 @@ -[ - { - "authority": null, - "contact": null, - "default-track": null, - "description": "A short description of the rock", - "id": "0soxpc4gipubAgTcDCAQ5AMlCbnu1Ion", - "links": {}, - "media": [], - "display_name": "grafana-agent", - "private": false, - "publisher": { - "display-name": "superdistro-admin-staging", - "id": "1dzBCortsJ3gk1wGP6XjxHewouTYWfC1", - "name": "superdistro-admin-staging", - "validation": "unproven" - }, - "status": "registered", - "store": "ubuntu", - "summary": null, - "title": null, - "track-guardrails": [], - "tracks": [ - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-03T08:00:38.580890", - "display_name": "22.04", - "version-pattern": null - }, - { - "automatic-phasing-percentage": null, - "created-at": "2024-09-03T08:40:58.845357", - "display_name": "latest", - "version-pattern": null - } - ], - "type": "rock", - "website": null - }, - { - "authority": null, - "contact": null, - "default-track": null, - "description": "A short description of the rock", - "id": "4eeyRdTL8bkSIRtGqOzE1EJz7mOsNqtv", - "links": {}, - "media": [], - "display_name": "bowenfan-chisel", - "private": false, - "publisher": { - "display-name": "superdistro-admin-staging", - "id": "1dzBCortsJ3gk1wGP6XjxHewouTYWfC1", - "name": "superdistro-admin-staging", - "validation": "star" - }, - "status": "registered", - "store": "ubuntu", - "summary": null, - "title": null, - "track-guardrails": [], - "tracks": [ - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-17T18:39:02.900239", - "display_name": "1.0-24.04", - "version-pattern": "*" - }, - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-17T18:38:57.735112", - "display_name": "latest", - "version-pattern": null - } - ], - "type": "rock", - "website": null - }, - { - "authority": null, - "contact": null, - "default-track": null, - "description": "A short description of the rock", - "id": "4qGuq5uBy7p2oV1R4RUmYR5ArLSL2BM7", - "links": {}, - "media": [], - "display_name": "starlark-rock-bowen", - "private": false, - "publisher": { - "display-name": "superdistro-admin-staging", - "id": "1dzBCortsJ3gk1wGP6XjxHewouTYWfC1", - "name": "superdistro-admin-staging", - "validation": "verified" - }, - "status": "registered", - "store": "ubuntu", - "summary": null, - "title": null, - "track-guardrails": [], - "tracks": [ - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-14T10:05:17.803607", - "display_name": "latest", - "version-pattern": null - } - ], - "type": "rock", - "website": null - }, - { - "authority": null, - "contact": null, - "default-track": null, - "description": "A short description of the rock", - "id": "65s7Naq8crN9OrcaMQziLLpzsMP7Kv0V", - "links": {}, - "media": [], - "display_name": "e2e-20241018005059", - "private": false, - "publisher": { - "display-name": "superdistro-admin-staging", - "id": "1dzBCortsJ3gk1wGP6XjxHewouTYWfC1", - "name": "superdistro-admin-staging", - "validation": "unproven" - }, - "status": "registered", - "store": "ubuntu", - "summary": null, - "title": null, - "track-guardrails": [], - "tracks": [ - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-18T00:51:10.016574", - "display_name": "v0.1-22.04", - "version-pattern": null - }, - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-18T00:51:08.158863", - "display_name": "latest", - "version-pattern": null - } - ], - "type": "rock", - "website": null - }, - { - "authority": null, - "contact": null, - "default-track": null, - "description": "A short description of the rock", - "id": "6M4DnEuOY4RXdo2opq5xMpCp74aEYTLf", - "links": {}, - "media": [], - "display_name": "e2e-20241017002554", - "private": false, - "publisher": { - "display-name": "superdistro-admin-staging", - "id": "1dzBCortsJ3gk1wGP6XjxHewouTYWfC1", - "name": "superdistro-admin-staging", - "validation": "star" - }, - "status": "registered", - "store": "ubuntu", - "summary": null, - "title": null, - "track-guardrails": [], - "tracks": [ - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-17T00:25:58.597693", - "display_name": "v0.1-22.04", - "version-pattern": null - }, - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-17T00:25:57.003796", - "display_name": "latest", - "version-pattern": null - } - ], - "type": "rock", - "website": null - }, - { - "authority": null, - "contact": null, - "default-track": null, - "description": "A short description of the rock", - "id": "9t6mvqPez6m3HqmT87LPIxI5zLDivphJ", - "links": {}, - "media": [], - "display_name": "e2e-20241018011905", - "private": false, - "publisher": { - "display-name": "superdistro-admin-staging", - "id": "1dzBCortsJ3gk1wGP6XjxHewouTYWfC1", - "name": "superdistro-admin-staging", - "validation": "verified" - }, - "status": "registered", - "store": "ubuntu", - "summary": null, - "title": null, - "track-guardrails": [], - "tracks": [ - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-18T01:19:10.203234", - "display_name": "v0.1-22.04", - "version-pattern": null - }, - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-18T01:19:08.793405", - "display_name": "latest", - "version-pattern": null - } - ], - "type": "rock", - "website": null - }, - { - "authority": null, - "contact": null, - "default-track": null, - "description": "A short description of the rock", - "id": "ApJO5crbqtqhlKaRRA5sWOwuPvr3rN60", - "links": {}, - "media": [], - "display_name": "grafana-agent2", - "private": false, - "publisher": { - "display-name": "superdistro-admin-staging", - "id": "1dzBCortsJ3gk1wGP6XjxHewouTYWfC1", - "name": "superdistro-admin-staging", - "validation": "unproven" - }, - "status": "registered", - "store": "ubuntu", - "summary": null, - "title": null, - "track-guardrails": [], - "tracks": [ - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-17T17:59:09.909864", - "display_name": "22.04", - "version-pattern": null - }, - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-17T17:41:37.399204", - "display_name": "latest", - "version-pattern": null - } - ], - "type": "rock", - "website": null - }, - { - "authority": null, - "contact": null, - "default-track": null, - "description": "A short description of the rock", - "id": "BDzKXN1tNTrAoOtWR8SV3M3wThgJZyM5", - "links": {}, - "media": [], - "display_name": "rock-aramanau-test-auth-postgres", - "private": false, - "publisher": { - "display-name": "superdistro-admin-staging", - "id": "1dzBCortsJ3gk1wGP6XjxHewouTYWfC1", - "name": "superdistro-admin-staging", - "validation": "unproven" - }, - "status": "registered", - "store": "ubuntu", - "summary": null, - "title": null, - "track-guardrails": [], - "tracks": [ - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-11T08:42:27.110489", - "display_name": "latest", - "version-pattern": null - } - ], - "type": "rock", - "website": null - }, - { - "authority": null, - "contact": null, - "default-track": null, - "description": "A short description of the rock", - "id": "Ez5AfcblFU9SVecIThdeYCuioTjbWxhD", - "links": {}, - "media": [], - "display_name": "rock-1009", - "private": false, - "publisher": { - "display-name": "superdistro-admin-staging", - "id": "1dzBCortsJ3gk1wGP6XjxHewouTYWfC1", - "name": "superdistro-admin-staging", - "validation": "unproven" - }, - "status": "registered", - "store": "ubuntu", - "summary": null, - "title": null, - "track-guardrails": [], - "tracks": [ - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-09T03:35:09.331993", - "display_name": "latest", - "version-pattern": null - } - ], - "type": "rock", - "website": null - }, - { - "authority": null, - "contact": null, - "default-track": null, - "description": "A short description of the rock", - "id": "GGU312dfxwrzRPjZxyD8hpcPuSaTD5vB", - "links": {}, - "media": [], - "display_name": "hello-new-rock2", - "private": false, - "publisher": { - "display-name": "superdistro-admin-staging", - "id": "1dzBCortsJ3gk1wGP6XjxHewouTYWfC1", - "name": "superdistro-admin-staging", - "validation": "unproven" - }, - "status": "registered", - "store": "ubuntu", - "summary": null, - "title": null, - "track-guardrails": [], - "tracks": [ - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-03T07:21:27.787072", - "display_name": "20.04", - "version-pattern": null - }, - { - "automatic-phasing-percentage": null, - "created-at": "2024-09-02T14:40:24.973421", - "display_name": "latest", - "version-pattern": null - } - ], - "type": "rock", - "website": null - }, - { - "authority": null, - "contact": null, - "default-track": null, - "description": "A short description of the rock", - "id": "IYV2OyHBfdlwrGrIsEOqbeW7AhPHZmuT", - "links": {}, - "media": [], - "display_name": "rock-aramanau-e2e-test", - "private": false, - "publisher": { - "display-name": "superdistro-admin-staging", - "id": "1dzBCortsJ3gk1wGP6XjxHewouTYWfC1", - "name": "superdistro-admin-staging", - "validation": "unproven" - }, - "status": "registered", - "store": "ubuntu", - "summary": null, - "title": null, - "track-guardrails": [], - "tracks": [ - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-25T09:42:23.093896", - "display_name": "latest", - "version-pattern": null - } - ], - "type": "rock", - "website": null - }, - { - "authority": null, - "contact": null, - "default-track": null, - "description": "A short description of the rock", - "id": "J8mOt4uHwNKi1ZgxwHZZlfMHcKEKsrwr", - "links": {}, - "media": [], - "display_name": "e2e-20241017002312", - "private": false, - "publisher": { - "display-name": "superdistro-admin-staging", - "id": "1dzBCortsJ3gk1wGP6XjxHewouTYWfC1", - "name": "superdistro-admin-staging", - "validation": "unproven" - }, - "status": "registered", - "store": "ubuntu", - "summary": null, - "title": null, - "track-guardrails": [], - "tracks": [ - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-17T00:24:58.802853", - "display_name": "v1-22.04", - "version-pattern": null - }, - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-17T00:23:17.047271", - "display_name": "latest", - "version-pattern": null - } - ], - "type": "rock", - "website": null - }, - { - "authority": null, - "contact": null, - "default-track": null, - "description": "A short description of the rock", - "id": "JWDdSwgYKakYLOlY4B6d7sS01HMa6DJe", - "links": {}, - "media": [], - "display_name": "shanepelletier-test-rock-hello2", - "private": false, - "publisher": { - "display-name": "superdistro-admin-staging", - "id": "1dzBCortsJ3gk1wGP6XjxHewouTYWfC1", - "name": "superdistro-admin-staging", - "validation": "unproven" - }, - "status": "registered", - "store": "ubuntu", - "summary": null, - "title": null, - "track-guardrails": [], - "tracks": [ - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-17T16:03:18.197898", - "display_name": "latest", - "version-pattern": null - } - ], - "type": "rock", - "website": null - }, - { - "authority": null, - "contact": null, - "default-track": null, - "description": "A short description of the rock", - "id": "KSqXjR7ag53SQUcWiG5xO2hG8RVuBwIu", - "links": {}, - "media": [], - "display_name": "superalpaca-test-0000002", - "private": false, - "publisher": { - "display-name": "superdistro-admin-staging", - "id": "1dzBCortsJ3gk1wGP6XjxHewouTYWfC1", - "name": "superdistro-admin-staging", - "validation": "unproven" - }, - "status": "registered", - "store": "ubuntu", - "summary": null, - "title": null, - "track-guardrails": [], - "tracks": [ - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-09T02:01:05.858351", - "display_name": "0.1-22.04", - "version-pattern": null - }, - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-09T02:00:37.457846", - "display_name": "latest", - "version-pattern": null - } - ], - "type": "rock", - "website": null - }, - { - "authority": null, - "contact": null, - "default-track": null, - "description": "A short description of the rock", - "id": "OIM9uUdB6IA23c7mW0ZNfZhLJL7pJz3b", - "links": {}, - "media": [], - "display_name": "shanepelletier-test-rockcraft-nonchiselled", - "private": false, - "publisher": { - "display-name": "superdistro-admin-staging", - "id": "1dzBCortsJ3gk1wGP6XjxHewouTYWfC1", - "name": "superdistro-admin-staging", - "validation": "unproven" - }, - "status": "registered", - "store": "ubuntu", - "summary": null, - "title": null, - "track-guardrails": [], - "tracks": [ - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-04T19:32:19.085347", - "display_name": "latest", - "version-pattern": null - } - ], - "type": "rock", - "website": null - }, - { - "authority": null, - "contact": null, - "default-track": null, - "description": "A short description of the rock", - "id": "OpIALfdTVPbyDG89Nb5XT2UHrCrM5KJX", - "links": {}, - "media": [], - "display_name": "ruha-test-rockcraft3", - "private": false, - "publisher": { - "display-name": "superdistro-admin-staging", - "id": "1dzBCortsJ3gk1wGP6XjxHewouTYWfC1", - "name": "superdistro-admin-staging", - "validation": "unproven" - }, - "status": "registered", - "store": "ubuntu", - "summary": null, - "title": null, - "track-guardrails": [], - "tracks": [ - { - "automatic-phasing-percentage": null, - "created-at": "2024-09-03T19:21:08.485355", - "display_name": "latest", - "version-pattern": null - } - ], - "type": "rock", - "website": null - }, - { - "authority": null, - "contact": null, - "default-track": null, - "description": "A short description of the rock", - "id": "OvywXeVyAHgOeGcNHkPMhPzL8GoXTHSf", - "links": {}, - "media": [], - "display_name": "ruha-test-rockcraft", - "private": false, - "publisher": { - "display-name": "superdistro-admin-staging", - "id": "1dzBCortsJ3gk1wGP6XjxHewouTYWfC1", - "name": "superdistro-admin-staging", - "validation": "unproven" - }, - "status": "registered", - "store": "ubuntu", - "summary": null, - "title": null, - "track-guardrails": [], - "tracks": [ - { - "automatic-phasing-percentage": null, - "created-at": "2024-08-06T18:33:09.871135", - "display_name": "latest", - "version-pattern": null - } - ], - "type": "rock", - "website": null - }, - { - "authority": null, - "contact": null, - "default-track": null, - "description": "A short description of the rock", - "id": "PzEFSJZUs5aBDpAGTBDHFiAAKh3NbU1R", - "links": {}, - "media": [], - "display_name": "e2e-20241018010253", - "private": false, - "publisher": { - "display-name": "superdistro-admin-staging", - "id": "1dzBCortsJ3gk1wGP6XjxHewouTYWfC1", - "name": "superdistro-admin-staging", - "validation": "unproven" - }, - "status": "registered", - "store": "ubuntu", - "summary": null, - "title": null, - "track-guardrails": [], - "tracks": [ - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-18T01:02:56.869418", - "display_name": "latest", - "version-pattern": null - } - ], - "type": "rock", - "website": null - }, - { - "authority": null, - "contact": null, - "default-track": null, - "description": "A short description of the rock", - "id": "USuhyUIYWxLZokhlZF6gL7nV9RQulPFj", - "links": {}, - "media": [], - "display_name": "e2e-20241018002023", - "private": false, - "publisher": { - "display-name": "superdistro-admin-staging", - "id": "1dzBCortsJ3gk1wGP6XjxHewouTYWfC1", - "name": "superdistro-admin-staging", - "validation": "unproven" - }, - "status": "registered", - "store": "ubuntu", - "summary": null, - "title": null, - "track-guardrails": [], - "tracks": [ - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-18T00:20:29.518767", - "display_name": "v0.1-22.04", - "version-pattern": null - }, - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-18T00:20:28.107591", - "display_name": "latest", - "version-pattern": null - } - ], - "type": "rock", - "website": null - }, - { - "authority": null, - "contact": null, - "default-track": null, - "description": "A short description of the rock", - "id": "WPrQ1muFmZGrd1XGaWuevWwSzu9KOHNx", - "links": {}, - "media": [], - "display_name": "shanepelletier-test-rockcraft", - "private": false, - "publisher": { - "display-name": "superdistro-admin-staging", - "id": "1dzBCortsJ3gk1wGP6XjxHewouTYWfC1", - "name": "superdistro-admin-staging", - "validation": "unproven" - }, - "status": "registered", - "store": "ubuntu", - "summary": null, - "title": null, - "track-guardrails": [], - "tracks": [ - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-04T19:06:39.607873", - "display_name": "latest", - "version-pattern": null - } - ], - "type": "rock", - "website": null - }, - { - "authority": null, - "contact": null, - "default-track": null, - "description": "A short description of the rock", - "id": "WmeywvX9tNxhKclgQCjBfpMraVnv7dGj", - "links": {}, - "media": [], - "display_name": "dariuszd-autossh14", - "private": false, - "publisher": { - "display-name": "superdistro-admin-staging", - "id": "1dzBCortsJ3gk1wGP6XjxHewouTYWfC1", - "name": "superdistro-admin-staging", - "validation": "unproven" - }, - "status": "registered", - "store": "ubuntu", - "summary": null, - "title": null, - "track-guardrails": [], - "tracks": [ - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-16T15:11:47.503274", - "display_name": "latest", - "version-pattern": null - } - ], - "type": "rock", - "website": null - }, - { - "authority": null, - "contact": null, - "default-track": null, - "description": "A short description of the rock", - "id": "WnTLGBSqDiF73ES0CipGYeD7XOUpp9QP", - "links": {}, - "media": [], - "display_name": "shanepelletier-hello2", - "private": false, - "publisher": { - "display-name": "superdistro-admin-staging", - "id": "1dzBCortsJ3gk1wGP6XjxHewouTYWfC1", - "name": "superdistro-admin-staging", - "validation": "unproven" - }, - "status": "registered", - "store": "ubuntu", - "summary": null, - "title": null, - "track-guardrails": [], - "tracks": [ - { - "automatic-phasing-percentage": null, - "created-at": "2024-09-03T19:23:08.395591", - "display_name": "latest", - "version-pattern": null - } - ], - "type": "rock", - "website": null - }, - { - "authority": null, - "contact": null, - "default-track": null, - "description": "A short description of the rock", - "id": "Xd9xedhufNj8YhsbUvNF8mRXVO9X8yoc", - "links": {}, - "media": [], - "display_name": "e2e-20241018020720", - "private": false, - "publisher": { - "display-name": "superdistro-admin-staging", - "id": "1dzBCortsJ3gk1wGP6XjxHewouTYWfC1", - "name": "superdistro-admin-staging", - "validation": "unproven" - }, - "status": "registered", - "store": "ubuntu", - "summary": null, - "title": null, - "track-guardrails": [], - "tracks": [ - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-18T02:07:28.082370", - "display_name": "v0.1-22.04", - "version-pattern": null - }, - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-18T02:07:26.397436", - "display_name": "latest", - "version-pattern": null - } - ], - "type": "rock", - "website": null - }, - { - "authority": null, - "contact": null, - "default-track": null, - "description": "A short description of the rock", - "id": "Y21riuYgHFkAG7EIv2HnuT48AFzten5u", - "links": {}, - "media": [], - "display_name": "e2e-20241018010301", - "private": false, - "publisher": { - "display-name": "superdistro-admin-staging", - "id": "1dzBCortsJ3gk1wGP6XjxHewouTYWfC1", - "name": "superdistro-admin-staging", - "validation": "unproven" - }, - "status": "registered", - "store": "ubuntu", - "summary": null, - "title": null, - "track-guardrails": [], - "tracks": [ - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-18T01:03:08.001639", - "display_name": "v0.1-22.04", - "version-pattern": null - }, - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-18T01:03:06.636293", - "display_name": "latest", - "version-pattern": null - } - ], - "type": "rock", - "website": null - }, - { - "authority": null, - "contact": null, - "default-track": null, - "description": "A short description of the rock", - "id": "dHIgic3TLaU5pIU09RinaEYo7miWCfRh", - "links": {}, - "media": [], - "display_name": "e2e-20241017234001", - "private": false, - "publisher": { - "display-name": "superdistro-admin-staging", - "id": "1dzBCortsJ3gk1wGP6XjxHewouTYWfC1", - "name": "superdistro-admin-staging", - "validation": "unproven" - }, - "status": "registered", - "store": "ubuntu", - "summary": null, - "title": null, - "track-guardrails": [], - "tracks": [ - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-17T23:40:11.831840", - "display_name": "v0.1-22.04", - "version-pattern": null - }, - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-17T23:40:08.904621", - "display_name": "latest", - "version-pattern": null - } - ], - "type": "rock", - "website": null - }, - { - "authority": null, - "contact": null, - "default-track": null, - "description": "A short description of the rock", - "id": "erXMAAaFZ0UpdgR3TADNrnVxEoc5BJd3", - "links": {}, - "media": [], - "display_name": "hello-new-rock", - "private": false, - "publisher": { - "display-name": "superdistro-admin-staging", - "id": "1dzBCortsJ3gk1wGP6XjxHewouTYWfC1", - "name": "superdistro-admin-staging", - "validation": "unproven" - }, - "status": "registered", - "store": "ubuntu", - "summary": null, - "title": null, - "track-guardrails": [], - "tracks": [ - { - "automatic-phasing-percentage": null, - "created-at": "2024-08-30T14:02:08.742829", - "display_name": "latest", - "version-pattern": null - } - ], - "type": "rock", - "website": null - }, - { - "authority": null, - "contact": null, - "default-track": null, - "description": "A short description of the rock", - "id": "kCFesqjYxVJQebnix7yU7QA3WLIa8qKR", - "links": {}, - "media": [], - "display_name": "shanepelletier-test-rock-hello", - "private": false, - "publisher": { - "display-name": "superdistro-admin-staging", - "id": "1dzBCortsJ3gk1wGP6XjxHewouTYWfC1", - "name": "superdistro-admin-staging", - "validation": "unproven" - }, - "status": "registered", - "store": "ubuntu", - "summary": null, - "title": null, - "track-guardrails": [], - "tracks": [ - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-17T15:54:38.686289", - "display_name": "latest", - "version-pattern": null - } - ], - "type": "rock", - "website": null - }, - { - "authority": null, - "contact": null, - "default-track": null, - "description": "A short description of the rock", - "id": "kKyoh9YhAlVlkM56stqxOFSvVIWGLJpU", - "links": {}, - "media": [], - "display_name": "e2e-20241018013130", - "private": false, - "publisher": { - "display-name": "superdistro-admin-staging", - "id": "1dzBCortsJ3gk1wGP6XjxHewouTYWfC1", - "name": "superdistro-admin-staging", - "validation": "unproven" - }, - "status": "registered", - "store": "ubuntu", - "summary": null, - "title": null, - "track-guardrails": [], - "tracks": [ - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-18T01:31:39.524677", - "display_name": "v0.1-22.04", - "version-pattern": null - }, - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-18T01:31:38.159226", - "display_name": "latest", - "version-pattern": null - } - ], - "type": "rock", - "website": null - }, - { - "authority": null, - "contact": null, - "default-track": null, - "description": "A short description of the rock", - "id": "klBi0WpetpvDjg8vs8JMdIHXPkEs4CyJ", - "links": {}, - "media": [], - "display_name": "e2e-20241017002239", - "private": false, - "publisher": { - "display-name": "superdistro-admin-staging", - "id": "1dzBCortsJ3gk1wGP6XjxHewouTYWfC1", - "name": "superdistro-admin-staging", - "validation": "unproven" - }, - "status": "registered", - "store": "ubuntu", - "summary": null, - "title": null, - "track-guardrails": [], - "tracks": [ - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-17T00:22:43.641334", - "display_name": "latest", - "version-pattern": null - } - ], - "type": "rock", - "website": null - }, - { - "authority": null, - "contact": null, - "default-track": null, - "description": "A short description of the rock", - "id": "mmiD5pBOo8SYaTW3K3TQDU4IFCYE99rM", - "links": {}, - "media": [], - "display_name": "rock-1009-2", - "private": false, - "publisher": { - "display-name": "superdistro-admin-staging", - "id": "1dzBCortsJ3gk1wGP6XjxHewouTYWfC1", - "name": "superdistro-admin-staging", - "validation": "unproven" - }, - "status": "registered", - "store": "ubuntu", - "summary": null, - "title": null, - "track-guardrails": [], - "tracks": [ - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-09T03:40:09.424582", - "display_name": "latest", - "version-pattern": null - } - ], - "type": "rock", - "website": null - }, - { - "authority": null, - "contact": null, - "default-track": null, - "description": "A short description of the rock", - "id": "nM0v2xP7RG3T7cAy3H3y6IAmgRwLyZQt", - "links": {}, - "media": [], - "display_name": "e2e-20241017042308", - "private": false, - "publisher": { - "display-name": "superdistro-admin-staging", - "id": "1dzBCortsJ3gk1wGP6XjxHewouTYWfC1", - "name": "superdistro-admin-staging", - "validation": "unproven" - }, - "status": "registered", - "store": "ubuntu", - "summary": null, - "title": null, - "track-guardrails": [], - "tracks": [ - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-17T04:23:28.198929", - "display_name": "v0.1-22.04", - "version-pattern": null - }, - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-17T04:23:26.738370", - "display_name": "latest", - "version-pattern": null - } - ], - "type": "rock", - "website": null - }, - { - "authority": null, - "contact": null, - "default-track": null, - "description": "A short description of the rock", - "id": "rCMz3ESIksZvlDzWelZ8QR58CLyhvrbs", - "links": {}, - "media": [], - "display_name": "e2e-20241018012941", - "private": false, - "publisher": { - "display-name": "superdistro-admin-staging", - "id": "1dzBCortsJ3gk1wGP6XjxHewouTYWfC1", - "name": "superdistro-admin-staging", - "validation": "unproven" - }, - "status": "registered", - "store": "ubuntu", - "summary": null, - "title": null, - "track-guardrails": [], - "tracks": [ - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-18T01:29:49.317603", - "display_name": "v0.1-22.04", - "version-pattern": null - }, - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-18T01:29:47.874410", - "display_name": "latest", - "version-pattern": null - } - ], - "type": "rock", - "website": null - }, - { - "authority": null, - "contact": null, - "default-track": null, - "description": "A short description of the rock", - "id": "vD735dPpm0Kz3CmR34C3SJq2qh90UbsN", - "links": {}, - "media": [], - "display_name": "mmm2", - "private": false, - "publisher": { - "display-name": "superdistro-admin-staging", - "id": "1dzBCortsJ3gk1wGP6XjxHewouTYWfC1", - "name": "superdistro-admin-staging", - "validation": "unproven" - }, - "status": "registered", - "store": "ubuntu", - "summary": null, - "title": null, - "track-guardrails": [], - "tracks": [ - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-09T03:51:56.730727", - "display_name": "latest", - "version-pattern": null - } - ], - "type": "rock", - "website": null - }, - { - "authority": null, - "contact": null, - "default-track": null, - "description": "A short description of the rock", - "id": "zyMmABPlI7dBcfSeJHYyklLBo98outsX", - "links": {}, - "media": [], - "display_name": "test11592", - "private": false, - "publisher": { - "display-name": "superdistro-admin-staging", - "id": "1dzBCortsJ3gk1wGP6XjxHewouTYWfC1", - "name": "superdistro-admin-staging", - "validation": "unproven" - }, - "status": "registered", - "store": "ubuntu", - "summary": null, - "title": null, - "track-guardrails": [], - "tracks": [ - { - "automatic-phasing-percentage": null, - "created-at": "2024-10-16T04:12:36.468338", - "display_name": "latest", - "version-pattern": null - } - ], - "type": "rock", - "website": null - } -] \ No newline at end of file From 645e3ba804ad5a484506f50b24f4b95c34012501 Mon Sep 17 00:00:00 2001 From: codeEmpress1 Date: Thu, 19 Jun 2025 10:53:30 +0300 Subject: [PATCH 08/24] chore: remove unused blueprint --- webapp/app.py | 2 - webapp/config.py | 11 -- webapp/packages/__init__.py | 0 webapp/packages/logic.py | 161 ------------------------------ webapp/packages/store_packages.py | 19 ---- 5 files changed, 193 deletions(-) delete mode 100644 webapp/packages/__init__.py delete mode 100644 webapp/packages/logic.py delete mode 100644 webapp/packages/store_packages.py diff --git a/webapp/app.py b/webapp/app.py index bd8b5f03..9fd0b583 100644 --- a/webapp/app.py +++ b/webapp/app.py @@ -8,7 +8,6 @@ from webapp.store.views import store from webapp.helpers import markdown_to_html from canonicalwebteam.flask_base.app import FlaskBase -from webapp.packages.store_packages import store_packages app = FlaskBase( @@ -49,7 +48,6 @@ def replace_with_link(match): csrf.init_app(app) -app.register_blueprint(store_packages) app.register_blueprint(store) diff --git a/webapp/config.py b/webapp/config.py index 1505c174..6da50e72 100644 --- a/webapp/config.py +++ b/webapp/config.py @@ -3,15 +3,4 @@ APP_NAME = "rockstore" DETAILS_VIEW_REGEX = "[A-Za-z0-9-]*[A-Za-z][A-Za-z0-9-]*" -SEARCH_FIELDS = [ - "result.categories", - "result.summary", - "result.media", - "result.title", - "result.publisher.display-name", - "default-release.revision.revision", - "default-release.channel", - "result.deployable-on", -] - SENTRY_DSN = os.getenv("SENTRY_DSN", "").strip() diff --git a/webapp/packages/__init__.py b/webapp/packages/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/webapp/packages/logic.py b/webapp/packages/logic.py deleted file mode 100644 index 63ee5659..00000000 --- a/webapp/packages/logic.py +++ /dev/null @@ -1,161 +0,0 @@ -import json -from typing import List, Dict, TypedDict, Any, Union - -from webapp.store.logic import format_slug - -Packages = TypedDict( - "Packages", - { - "packages": List[ - Dict[ - str, - Union[Dict[str, Union[str, List[str]]], List[Dict[str, str]]], - ] - ] - }, -) - -Package = TypedDict( - "Package", - { - "package": Dict[ - str, Union[Dict[str, str], List[str], List[Dict[str, str]]] - ] - }, -) - - -def get_icon(media): - icons = [m["url"] for m in media if m["type"] == "icon"] - if len(icons) > 0: - return icons[0] - return "" - - -def parse_package_for_card( - package: Dict[str, Any], -) -> Package: - """ - Parses a package and returns the formatted package - based on the given card schema. - - :param: package (Dict[str, Any]): The package to be parsed. - :returns: a dictionary containing the formatted package. - - note: - - This function has to be refactored to be more generic, - so we won't have to check for the package type before parsing. - - """ - - resp = { - "package": { - "description": "", - "display_name": "", - "icon_url": "", - "name": "", - "platforms": [], - "type": "", - "website": "", - "tracks": [], - "track_guardrails": [], - "status": "", - "private": False, - "contact": "", - }, - "publisher": {"display_name": "", "name": "", "validation": ""}, - "ratings": {"value": "0", "count": "0"}, - } - - publisher = package.get("publisher", {}) - resp["package"]["type"] = package.get("type", "") - resp["package"]["name"] = package.get("name", "") - resp["package"]["description"] = ( - package["summary"] or package["description"] - ) - resp["package"]["display_name"] = package.get( - "display_name", format_slug(package.get("name", "")) - ) - resp["package"]["name"] = package.get( - "display_name", format_slug(package.get("name", "")) - ) - resp["publisher"]["display_name"] = publisher.get("display-name", "") - resp["publisher"]["validation"] = publisher.get("validation", "") - resp["package"]["icon_url"] = get_icon(package.get("media", [])) - resp["package"]["website"] = package.get("website", "") - resp["package"]["status"] = package.get("status", "") - resp["package"]["private"] = package.get("private", False) - resp["package"]["contact"] = package.get("contact", "") - - return resp - - -def paginate( - packages: List[Packages], page: int, size: int, total_pages: int -) -> List[Packages]: - """ - Paginates a list of packages based on the specified page and size. - - :param: packages (List[Packages]): The list of packages to paginate. - :param: page (int): The current page number. - :param: size (int): The number of packages to include in each page. - :param: total_pages (int): The total number of pages. - :returns: a list of paginated packages. - - note: - - If the provided page exceeds the total number of pages, the last - page will be returned. - - If the provided page is less than 1, the first page will be returned. - """ - - if page > total_pages: - page = total_pages - if page < 1: - page = 1 - - start = (page - 1) * size - end = start + size - if end > len(packages): - end = len(packages) - - return packages[start:end] - - -def get_packages( - size: int = 10, - query_params: Dict[str, Any] = {}, -) -> List[Dict[str, Any]]: - """ - Retrieves a list of packages from the store based on the specified - parameters.The returned packages are paginated and parsed using the - card schema. - - """ - - with open("webapp/rocks.json", "r") as rocks: - packages = json.load(rocks) - rocks.close() - - for rock in packages: - rock[ - "icon_url" - ] = """ - https://api.charmhub.io/api/v1/media/download/ - charm_3uPxmv77o1PrixpQFIf8o7SkOLsnMWmZ_icon_ad1 - a94cf9bb9f68614cb6c17e54e2fbd9dcc7fecc514dc6012b7f58fb5b87f8f.png - """ - - total_pages = -(len(packages) // -size) - - total_pages = -(len(packages) // -size) - total_items = len(packages) - page = int(query_params.get("page", 1)) - packages_per_page = paginate(packages, page, size, total_pages) - parsed_packages = [] - for package in packages_per_page: - parsed_packages.append(parse_package_for_card(package)) - return { - "packages": parsed_packages, - "total_pages": total_pages, - "total_items": total_items, - } diff --git a/webapp/packages/store_packages.py b/webapp/packages/store_packages.py deleted file mode 100644 index 02d74aca..00000000 --- a/webapp/packages/store_packages.py +++ /dev/null @@ -1,19 +0,0 @@ -from flask import ( - Blueprint, - request, - make_response, -) - -from webapp.packages.logic import get_packages - -store_packages = Blueprint( - "package", - __name__, -) - - -@store_packages.route("/store.json") -def get_store_packages(): - args = dict(request.args) - res = make_response(get_packages(12, args)) - return res From 5cbc0a91e9de88a18e24053b7523bb0bc21c4c01 Mon Sep 17 00:00:00 2001 From: codeEmpress1 Date: Thu, 19 Jun 2025 11:00:09 +0300 Subject: [PATCH 09/24] fix lint --- webapp/store/logic.py | 48 ++++++++++++++++++++++++++++--------------- webapp/store/views.py | 4 ++-- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/webapp/store/logic.py b/webapp/store/logic.py index 5d5c4f3e..0469690e 100644 --- a/webapp/store/logic.py +++ b/webapp/store/logic.py @@ -21,16 +21,16 @@ ] DETAILS_FIELDS = [ - "categories", - "contact", - "description", - "license", - "links", - "media", - "private", - "publisher", - "summary", - "title", + "categories", + "contact", + "description", + "license", + "links", + "media", + "private", + "publisher", + "summary", + "title", "website", "created-at", "download", @@ -63,6 +63,7 @@ }, ) + def convert_date(date_to_convert): """ Convert date to human readable format: Month Day Year @@ -82,6 +83,7 @@ def convert_date(date_to_convert): else: return date_parsed.strftime("%d %b %Y") + def format_relative_date(date_str: str) -> str: """ Converts an ISO 8601 date string to a human-readable relative format. @@ -145,12 +147,14 @@ def format_slug(slug): .replace("Iot", "IoT") ) + def get_icon(media): icons = [m["url"] for m in media if m["type"] == "icon"] if len(icons) > 0: return icons[0] return "" + def parse_package_for_card( package: Dict[str, Any], ) -> Package: @@ -190,7 +194,9 @@ def parse_package_for_card( metadata["summary"] or metadata["description"] ) resp["package"]["display_name"] = format_slug(metadata.get("title", "")) - resp["package"]["icon_url"] = get_icon(metadata.get("links", {}).get("media", [])) + resp["package"]["icon_url"] = get_icon( + metadata.get("links", {}).get("media", []) + ) resp["package"]["website"] = metadata.get("website", "") resp["package"]["contact"] = metadata.get("contact", "") @@ -200,6 +206,7 @@ def parse_package_for_card( return resp + def paginate(packages: List[Packages], page: int, size: int) -> List[Packages]: """ Paginates the list of packages safely based on page and size. @@ -219,6 +226,7 @@ def paginate(packages: List[Packages], page: int, size: int) -> List[Packages]: return packages[start:end] + def parse_rock_details(rock): parsed_rock = { "display_name": "", @@ -239,11 +247,12 @@ def parse_rock_details(rock): "upstream_details": {}, "related_rocks": [], "downstream_artifacts": {}, - }, "categories": rock["metadata"].get("categories", []), "publisher": { - "name": format_slug(rock["metadata"]["publisher"].get("display-name", "")), + "name": format_slug( + rock["metadata"]["publisher"].get("display-name", "") + ), "username": rock["metadata"]["publisher"].get("username", ""), "validation": rock["metadata"]["publisher"].get("validation", ""), }, @@ -264,7 +273,9 @@ def parse_rock_details(rock): parsed_channel = { "workload_version": normalized_version, "risk": channel_data.get("risk", ""), - "last_updated": format_relative_date(channel_data.get("released-at", "")), + "last_updated": format_relative_date( + channel_data.get("released-at", "") + ), "released_at": convert_date(channel_data.get("released-at", "")), "revision": revision_data["revision"], "version": revision_data.get("version", ""), @@ -273,11 +284,13 @@ def parse_rock_details(rock): parsed_rock["channels"].append(parsed_channel) parsed_rock["latest_channel"] = max( parsed_rock["channels"], - key=lambda x: datetime.datetime.strptime(x["released_at"], "%d %b %Y"), + key=lambda x: datetime.datetime.strptime( + x["released_at"], "%d %b %Y" + ), ) - return parsed_rock + def get_rocks( size: int = 10, query_params: Dict[str, Any] = {}, @@ -290,7 +303,7 @@ def get_rocks( """ rocks2 = device_gw.find("%", fields=FIND_FIELDS).get("results", []) - + total_items = len(rocks2) total_pages = (total_items + size - 1) // size page = int(query_params.get("page", 1)) @@ -305,6 +318,7 @@ def get_rocks( "total_items": total_items, } + def get_rock( entity_name: str, ) -> Dict[str, Any]: diff --git a/webapp/store/views.py b/webapp/store/views.py index d70266cd..73361e7e 100644 --- a/webapp/store/views.py +++ b/webapp/store/views.py @@ -1,5 +1,3 @@ -import json - from flask import ( Blueprint, request, @@ -15,12 +13,14 @@ "store", __name__, template_folder="/templates", static_folder="/static" ) + @store.route("/store.json") def get_store_packages(): args = dict(request.args) res = make_response(get_rocks(12, args)) return res + @store.route('/') def details_overview(entity_name): rock = parse_rock_details(get_rock(entity_name)) From 7b538f4fddf1e61455a7732562b2c2a20e0493dd Mon Sep 17 00:00:00 2001 From: codeEmpress1 Date: Thu, 19 Jun 2025 11:22:02 +0300 Subject: [PATCH 10/24] add page params for fetching packages --- static/js/src/store/components/Packages/Packages.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/js/src/store/components/Packages/Packages.tsx b/static/js/src/store/components/Packages/Packages.tsx index 8b515cbe..05100c2f 100644 --- a/static/js/src/store/components/Packages/Packages.tsx +++ b/static/js/src/store/components/Packages/Packages.tsx @@ -11,7 +11,7 @@ function Packages() { const ITEMS_PER_PAGE = 12; const getData = async () => { - const response = await fetch(`/store.json`); + const response = await fetch(`/store.json?page=${searchParams.get("page") || "1"}`,); const data = await response.json(); const packagesWithId = data.packages.map((item: string[]) => ({ From e0ef609a92d42059605d807be5fcce6e51e388f2 Mon Sep 17 00:00:00 2001 From: codeEmpress1 Date: Thu, 19 Jun 2025 11:24:11 +0300 Subject: [PATCH 11/24] update details page --- templates/details/details_layout.html | 3 +-- templates/details/overview.html | 37 +++++++++++++++++++++++---- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/templates/details/details_layout.html b/templates/details/details_layout.html index bd47aab8..5916e09d 100644 --- a/templates/details/details_layout.html +++ b/templates/details/details_layout.html @@ -5,13 +5,12 @@ {% block meta_image_width %}200{% endblock %} {% block meta_image_height %}200{% endblock %} - {% block structure_data_markup %} From 9f6dc37fb76adec113488774be88fb60c96c7648 Mon Sep 17 00:00:00 2001 From: codeEmpress1 Date: Thu, 19 Jun 2025 11:46:41 +0300 Subject: [PATCH 15/24] fix js lint --- static/js/src/store/components/Packages/Packages.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/static/js/src/store/components/Packages/Packages.tsx b/static/js/src/store/components/Packages/Packages.tsx index 05100c2f..d3cc7c95 100644 --- a/static/js/src/store/components/Packages/Packages.tsx +++ b/static/js/src/store/components/Packages/Packages.tsx @@ -11,7 +11,9 @@ function Packages() { const ITEMS_PER_PAGE = 12; const getData = async () => { - const response = await fetch(`/store.json?page=${searchParams.get("page") || "1"}`,); + const response = await fetch( + `/store.json?page=${searchParams.get("page") || "1"}` + ); const data = await response.json(); const packagesWithId = data.packages.map((item: string[]) => ({ From 4743490510c272275c17121b5b170ff047a22462 Mon Sep 17 00:00:00 2001 From: codeEmpress1 Date: Thu, 19 Jun 2025 12:26:30 +0300 Subject: [PATCH 16/24] add unit tests for store logic --- tests/store/__init__.py | 0 tests/store/test_store_logic.py | 81 +++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 tests/store/__init__.py create mode 100644 tests/store/test_store_logic.py diff --git a/tests/store/__init__.py b/tests/store/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/store/test_store_logic.py b/tests/store/test_store_logic.py new file mode 100644 index 00000000..3d2c4512 --- /dev/null +++ b/tests/store/test_store_logic.py @@ -0,0 +1,81 @@ +import unittest +import datetime +from webapp.store.logic import ( + convert_date, + format_relative_date, + get_icon, + format_slug, + parse_package_for_card, + paginate, +) + +class TestStoreLogic(unittest.TestCase): + + def test_convert_date_today(self): + today = datetime.datetime.now(datetime.timezone.utc).isoformat() + result = convert_date(today) + self.assertIn(result, ["Today", "Yesterday"]) + + def test_format_relative_date_today(self): + now = datetime.datetime.now(datetime.timezone.utc).isoformat() + self.assertEqual(format_relative_date(now), "today") + + def test_format_relative_date_yesterday(self): + yesterday = (datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=1)).isoformat() + self.assertEqual(format_relative_date(yesterday), "yesterday") + + def test_format_relative_date_weeks(self): + old_date = (datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=14)).isoformat() + self.assertEqual(format_relative_date(old_date), "2 weeks ago") + + def test_format_relative_date_months(self): + old_date = (datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=60)).isoformat() + self.assertEqual(format_relative_date(old_date), "2 months ago") + + def test_get_icon_with_icons(self): + media = [{"type": "icon", "url": "https://example.com/icon.png"}] + self.assertEqual(get_icon(media), "https://example.com/icon.png") + + def test_format_slug(self): + self.assertEqual(format_slug("hello-world_iot_and_some_random_text"), "Hello World IoT and Some Random Text") + + def test_parse_package_for_card(self): + package = { + "name": "test-package", + "metadata": { + "summary": "A test summary", + "description": "A test description", + "title": "test-title", + "website": "https://example.com", + "contact": "test@example.com", + "links": { + "media": [{"type": "icon", "url": "https://example.com/icon.png"}] + }, + "publisher": { + "display-name": "Test Publisher", + "username": "testuser", + "validation": "verified" + } + } + } + result = parse_package_for_card(package) + self.assertEqual(result["package"]["name"], "test-package") + self.assertEqual(result["package"]["display_name"], "Test Title") + self.assertEqual(result["package"]["icon_url"], "https://example.com/icon.png") + self.assertEqual(result["publisher"]["name"], "testuser") + + def test_paginate_bounds(self): + packages = [{"name": f"pkg{i}"} for i in range(25)] + page = 2 + size = 10 + paged = paginate(packages, page, size) + self.assertEqual(len(paged), 10) + self.assertEqual(paged[0]["name"], "pkg10") + + def test_paginate_overflow(self): + packages = [{"name": f"pkg{i}"} for i in range(5)] + paged = paginate(packages, page=10, size=2) + self.assertTrue(len(paged) <= 2) + +if __name__ == "__main__": + unittest.main() From 20b67f70e215e60570562775d9bc876738135212 Mon Sep 17 00:00:00 2001 From: codeEmpress1 Date: Thu, 19 Jun 2025 12:26:59 +0300 Subject: [PATCH 17/24] update docstrings for store logic --- webapp/store/logic.py | 68 +++++++++---------------------------------- 1 file changed, 14 insertions(+), 54 deletions(-) diff --git a/webapp/store/logic.py b/webapp/store/logic.py index 0469690e..1284086a 100644 --- a/webapp/store/logic.py +++ b/webapp/store/logic.py @@ -66,15 +66,7 @@ def convert_date(date_to_convert): """ - Convert date to human readable format: Month Day Year - - If date is less than a day return: today or yesterday - - Format of date to convert: 2019-01-12T16:48:41.821037+00:00 - Output: Jan 12 2019 - - :param date_to_convert: Date to convert - :returns: Readable date + Convert a datetime string to a human-readable string (e.g. 'Today', 'Yesterday', or '12 Jan 2023'). """ date_parsed = parser.parse(date_to_convert).replace(tzinfo=None) delta = datetime.datetime.now() - datetime.timedelta(days=1) @@ -86,21 +78,7 @@ def convert_date(date_to_convert): def format_relative_date(date_str: str) -> str: """ - Converts an ISO 8601 date string to a human-readable relative format. - - Examples: - - "today" - - "yesterday" - - "last 3 days" - - "last 2 weeks" - - "last 1 month" - - "25 Apr 2025" (for dates > ~3 months ago) - - Args: - date_str (str): ISO 8601 datetime string (with timezone) - - Returns: - str: Human-readable relative date + Return a relative time string from an ISO datetime string (e.g. '2 weeks ago', '25 Apr 2025'). """ try: given_date = datetime.datetime.fromisoformat(date_str) @@ -129,16 +107,17 @@ def format_relative_date(date_str: str) -> str: def get_icons(package): + """ + Extracts a list of icon URLs from the package metadata. + """ media = package["result"]["media"] return [m["url"] for m in media if m["type"] == "icon"] def format_slug(slug): - """Format slug name into a standard title format - :param slug: The hypen spaced, lowercase slug to be formatted - :return: The formatted string """ - + Converts a slug (e.g. 'my-app-name') to a title-like string (e.g. 'My App Name'). + """ return ( slug.title() .replace("-", " ") @@ -159,18 +138,8 @@ def parse_package_for_card( package: Dict[str, Any], ) -> Package: """ - Parses a package and returns the formatted package - based on the given card schema. - - :param: package (Dict[str, Any]): The package to be parsed. - :returns: a dictionary containing the formatted package. - - note: - - This function has to be refactored to be more generic, - so we won't have to check for the package type before parsing. - + Parses a package dictionary into a simplified schema for card display. """ - resp = { "package": { "description": "", @@ -209,9 +178,7 @@ def parse_package_for_card( def paginate(packages: List[Packages], page: int, size: int) -> List[Packages]: """ - Paginates the list of packages safely based on page and size. - - Returns only the correct slice of items for the given page. + Paginate the given packages list based on current page and size. """ total_items = len(packages) total_pages = (total_items + size - 1) // size # ceiling division @@ -228,6 +195,9 @@ def paginate(packages: List[Packages], page: int, size: int) -> List[Packages]: def parse_rock_details(rock): + """ + Parses detailed rock metadata into a structured format for internal use. + """ parsed_rock = { "display_name": "", "name": rock.get("name", ""), @@ -263,6 +233,7 @@ def parse_rock_details(rock): parsed_rock["icon_url"] = get_icon(rock["metadata"].get("media", [])) parsed_rock["license"] = rock["metadata"].get("license", "") + # Build channel info for channel in rock.get("channel-map", []): channel_data = channel.get("channel", {}) revision_data = channel.get("revision", {}) @@ -296,12 +267,8 @@ def get_rocks( query_params: Dict[str, Any] = {}, ) -> List[Dict[str, Any]]: """ - Retrieves a list of packages from the store based on the specified - parameters.The returned packages are paginated and parsed using the - card schema. - + Fetches paginated and parsed rock packages using DeviceGW. """ - rocks2 = device_gw.find("%", fields=FIND_FIELDS).get("results", []) total_items = len(rocks2) @@ -324,13 +291,6 @@ def get_rock( ) -> Dict[str, Any]: """ Retrieves a specific rock package by its name. - - :param: entity_name (str): The name of the rock package to retrieve. - :returns: a dictionary containing the rock package details. - - note: - - This function has to be refactored to be more generic, - so we won't have to check for the package type before parsing. """ rock = device_gw.get_item_details(entity_name, fields=DETAILS_FIELDS) if not rock: From 97b6e7ae59b9bb5baa74bc4b8e36e384705a325f Mon Sep 17 00:00:00 2001 From: codeEmpress1 Date: Thu, 19 Jun 2025 12:38:04 +0300 Subject: [PATCH 18/24] add unittests for store routes --- tests/store/test_store_routes.py | 54 ++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 tests/store/test_store_routes.py diff --git a/tests/store/test_store_routes.py b/tests/store/test_store_routes.py new file mode 100644 index 00000000..5ceec9c5 --- /dev/null +++ b/tests/store/test_store_routes.py @@ -0,0 +1,54 @@ +import unittest +from unittest.mock import patch +from webapp.app import app + +class StoreRouteTests(unittest.TestCase): + def setUp(self): + self.client = app.test_client() + + @patch("webapp.store.views.get_rocks") + def test_get_store_packages_json(self, mock_get_rocks): + mock_get_rocks.return_value = {"packages": [], "total_pages": 1, "total_items": 0} + response = self.client.get("/store.json") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json["total_pages"], 1) + + @patch("webapp.store.views.get_rock") + def test_store_details(self, mock_get_rock): + mock_get_rock.return_value = { + "name": "sample-rock", + "metadata": { + "description": "Test rock", + "summary": "A summary", + "license": "Apache-2.0", + "links": {}, + "media": [], + "private": False, + "publisher": { + "display-name": "Test Publisher", + "username": "testuser", + "validation": "verified", + }, + "categories": [], + }, + "channel-map": [ + { + "channel": { + "risk": "stable", + "released-at": "2024-06-01T12:00:00+00:00", + "track": "latest" + }, + "revision": { + "version": "1.0.0", + "revision": 1 + } + } + ] + } + + response = self.client.get("/sample-rock") + self.assertEqual(response.status_code, 200) + response_text = response.get_data(as_text=True) + self.assertIn("sample-rock", response_text) + self.assertIn("Test rock", response_text) + \ No newline at end of file From 04c90e29d8bab612c8296cdac8501ae7c5f95667 Mon Sep 17 00:00:00 2001 From: codeEmpress1 Date: Thu, 19 Jun 2025 12:56:17 +0300 Subject: [PATCH 19/24] fix python lints --- tests/store/test_store_logic.py | 41 ++++++++++++++++++++------------ tests/store/test_store_routes.py | 20 +++++++++------- webapp/store/logic.py | 9 ++++--- 3 files changed, 44 insertions(+), 26 deletions(-) diff --git a/tests/store/test_store_logic.py b/tests/store/test_store_logic.py index 3d2c4512..aa730994 100644 --- a/tests/store/test_store_logic.py +++ b/tests/store/test_store_logic.py @@ -1,5 +1,5 @@ import unittest -import datetime +from datetime import datetime, timedelta, timezone from webapp.store.logic import ( convert_date, format_relative_date, @@ -9,27 +9,34 @@ paginate, ) + class TestStoreLogic(unittest.TestCase): def test_convert_date_today(self): - today = datetime.datetime.now(datetime.timezone.utc).isoformat() + today = datetime.now(timezone.utc).isoformat() result = convert_date(today) self.assertIn(result, ["Today", "Yesterday"]) def test_format_relative_date_today(self): - now = datetime.datetime.now(datetime.timezone.utc).isoformat() + now = datetime.now(timezone.utc).isoformat() self.assertEqual(format_relative_date(now), "today") def test_format_relative_date_yesterday(self): - yesterday = (datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=1)).isoformat() + yesterday = ( + datetime.now(timezone.utc) - timedelta(days=1) + ).isoformat() self.assertEqual(format_relative_date(yesterday), "yesterday") def test_format_relative_date_weeks(self): - old_date = (datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=14)).isoformat() + old_date = ( + datetime.now(timezone.utc) - timedelta(days=14) + ).isoformat() self.assertEqual(format_relative_date(old_date), "2 weeks ago") def test_format_relative_date_months(self): - old_date = (datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=60)).isoformat() + old_date = ( + datetime.now(timezone.utc) - timedelta(days=60) + ).isoformat() self.assertEqual(format_relative_date(old_date), "2 months ago") def test_get_icon_with_icons(self): @@ -37,7 +44,10 @@ def test_get_icon_with_icons(self): self.assertEqual(get_icon(media), "https://example.com/icon.png") def test_format_slug(self): - self.assertEqual(format_slug("hello-world_iot_and_some_random_text"), "Hello World IoT and Some Random Text") + self.assertEqual( + format_slug("hello-world_iot_and_some_random_text"), + "Hello World IoT and Some Random Text", + ) def test_parse_package_for_card(self): package = { @@ -49,19 +59,23 @@ def test_parse_package_for_card(self): "website": "https://example.com", "contact": "test@example.com", "links": { - "media": [{"type": "icon", "url": "https://example.com/icon.png"}] + "media": [ + {"type": "icon", "url": "https://example.com/icon.png"} + ] }, "publisher": { "display-name": "Test Publisher", "username": "testuser", - "validation": "verified" - } - } + "validation": "verified", + }, + }, } result = parse_package_for_card(package) self.assertEqual(result["package"]["name"], "test-package") self.assertEqual(result["package"]["display_name"], "Test Title") - self.assertEqual(result["package"]["icon_url"], "https://example.com/icon.png") + self.assertEqual( + result["package"]["icon_url"], "https://example.com/icon.png" + ) self.assertEqual(result["publisher"]["name"], "testuser") def test_paginate_bounds(self): @@ -76,6 +90,3 @@ def test_paginate_overflow(self): packages = [{"name": f"pkg{i}"} for i in range(5)] paged = paginate(packages, page=10, size=2) self.assertTrue(len(paged) <= 2) - -if __name__ == "__main__": - unittest.main() diff --git a/tests/store/test_store_routes.py b/tests/store/test_store_routes.py index 5ceec9c5..cc85643e 100644 --- a/tests/store/test_store_routes.py +++ b/tests/store/test_store_routes.py @@ -1,6 +1,7 @@ import unittest from unittest.mock import patch -from webapp.app import app +from webapp.app import app + class StoreRouteTests(unittest.TestCase): def setUp(self): @@ -8,7 +9,11 @@ def setUp(self): @patch("webapp.store.views.get_rocks") def test_get_store_packages_json(self, mock_get_rocks): - mock_get_rocks.return_value = {"packages": [], "total_pages": 1, "total_items": 0} + mock_get_rocks.return_value = { + "packages": [], + "total_pages": 1, + "total_items": 0, + } response = self.client.get("/store.json") self.assertEqual(response.status_code, 200) self.assertEqual(response.json["total_pages"], 1) @@ -36,14 +41,14 @@ def test_store_details(self, mock_get_rock): "channel": { "risk": "stable", "released-at": "2024-06-01T12:00:00+00:00", - "track": "latest" + "track": "latest", }, "revision": { - "version": "1.0.0", - "revision": 1 - } + "version": "1.0", + "revision": 1, + }, } - ] + ], } response = self.client.get("/sample-rock") @@ -51,4 +56,3 @@ def test_store_details(self, mock_get_rock): response_text = response.get_data(as_text=True) self.assertIn("sample-rock", response_text) self.assertIn("Test rock", response_text) - \ No newline at end of file diff --git a/webapp/store/logic.py b/webapp/store/logic.py index 1284086a..59478bb9 100644 --- a/webapp/store/logic.py +++ b/webapp/store/logic.py @@ -66,7 +66,8 @@ def convert_date(date_to_convert): """ - Convert a datetime string to a human-readable string (e.g. 'Today', 'Yesterday', or '12 Jan 2023'). + Convert a datetime string to a human-readable string. + (e.g. 'Today', 'Yesterday', or '12 Jan 2023'). """ date_parsed = parser.parse(date_to_convert).replace(tzinfo=None) delta = datetime.datetime.now() - datetime.timedelta(days=1) @@ -78,7 +79,8 @@ def convert_date(date_to_convert): def format_relative_date(date_str: str) -> str: """ - Return a relative time string from an ISO datetime string (e.g. '2 weeks ago', '25 Apr 2025'). + Return a relative time string from an ISO datetime string. + (e.g. '2 weeks ago', '25 Apr 2025'). """ try: given_date = datetime.datetime.fromisoformat(date_str) @@ -116,7 +118,8 @@ def get_icons(package): def format_slug(slug): """ - Converts a slug (e.g. 'my-app-name') to a title-like string (e.g. 'My App Name'). + Converts a slug to a title-like string. + (e.g. 'my-rock-name' to 'My Rock Name'). """ return ( slug.title() From e91588f460aa2394a2ad97ebcc5f10b403351f9f Mon Sep 17 00:00:00 2001 From: codeEmpress1 Date: Fri, 20 Jun 2025 10:48:15 +0300 Subject: [PATCH 20/24] address comments on previous PR --- .../store/components/Packages/Packages.tsx | 16 ++------ templates/details/_details-header.html | 39 +++++++------------ templates/details/_side-nav.html | 8 ++-- webapp/store/logic.py | 12 ++---- 4 files changed, 27 insertions(+), 48 deletions(-) diff --git a/static/js/src/store/components/Packages/Packages.tsx b/static/js/src/store/components/Packages/Packages.tsx index d3cc7c95..5d1559ca 100644 --- a/static/js/src/store/components/Packages/Packages.tsx +++ b/static/js/src/store/components/Packages/Packages.tsx @@ -1,4 +1,3 @@ -import { useEffect } from "react"; import { useQuery } from "react-query"; import { useSearchParams } from "react-router-dom"; import { Strip, Row, Col, Pagination } from "@canonical/react-components"; @@ -9,11 +8,11 @@ import { Package, Publisher } from "../../types"; function Packages() { const ITEMS_PER_PAGE = 12; + const [searchParams, setSearchParams] = useSearchParams(); + const currentPage = searchParams.get("page") || "1"; const getData = async () => { - const response = await fetch( - `/store.json?page=${searchParams.get("page") || "1"}` - ); + const response = await fetch(`/store.json?page=${currentPage}`); const data = await response.json(); const packagesWithId = data.packages.map((item: string[]) => ({ @@ -28,14 +27,7 @@ function Packages() { }; }; - const [searchParams, setSearchParams] = useSearchParams(); - const currentPage = searchParams.get("page") || "1"; - - const { data, status, refetch, isFetching } = useQuery("data", getData); - - useEffect(() => { - refetch(); - }, [searchParams]); + const { data, status, isFetching } = useQuery(["data", currentPage], getData); const firstResultNumber = (parseInt(currentPage) - 1) * ITEMS_PER_PAGE + 1; const lastResultNumber = diff --git a/templates/details/_details-header.html b/templates/details/_details-header.html index 8162cbb7..954a22f9 100644 --- a/templates/details/_details-header.html +++ b/templates/details/_details-header.html @@ -6,35 +6,26 @@
- {% if package["icon_url"] %} - {{ image(url=package.icon_url, - alt=package.name, - width="100", - height="100", - hi_def=True, - fill=True, - attrs={"class": "p-media-object__image"},) | safe - }} - {% else %} - {{ image(url="https://assets.ubuntu.com/v1/be6eb412-snapcraft-missing-icon.svg", - alt=package.name, - width="100", - height="100", - hi_def=True, - fill=True, - attrs={"class": "p-media-object__image"},) | safe - }} - {% endif %} + {{ image(url=package.icon_url or "https://assets.ubuntu.com/v1/be6eb412-snapcraft-missing-icon.svg", + alt=package.name, + width="100", + height="100", + hi_def=True, + fill=True, + attrs={"class": "p-media-object__image"} + ) | safe + }}

{{ package["display_name"] }}

- By - {% if package.publisher.name != None %}{{ package.publisher.name }}{% endif %} + {% if package.publisher.name %} + By {{ package.publisher.name }} + {% endif %} {% if package.publisher.validation == "verified" %} - - - Verified account + + Verified account badge + Verified account {% endif %}

diff --git a/templates/details/_side-nav.html b/templates/details/_side-nav.html index 96840b1c..57c14e1e 100644 --- a/templates/details/_side-nav.html +++ b/templates/details/_side-nav.html @@ -33,13 +33,13 @@

{{ nav_group.navlink_text }}

{% endif %} {% if package.metadata.license and not package.metadata.license == "unset" %} -

Rock License

+

Rock License

  {{ package.metadata.license }}


{% endif %} {% if package.metadata.rock_details %} -

Rock details

+

Rock details

    {% if package.metadata.status == "full" %}
  • @@ -81,7 +81,7 @@

    Websites


    {% endif %} {% if package.metadata["links"]["contact"] %} -

    Contact

    +

    Contact

      {% for contact in package.metadata["links"]["contact"] %}
    • {{ contact|replace('mailto:', '') }}
    • @@ -90,7 +90,7 @@

      Contact


      {% endif %} {% endif %} -

      Discuss this {{ package.type }}

      +

      Discuss this Rock

      Share your thoughts on this rock with the community on discourse.

      Join the discussion

      diff --git a/webapp/store/logic.py b/webapp/store/logic.py index 59478bb9..3d7f61ad 100644 --- a/webapp/store/logic.py +++ b/webapp/store/logic.py @@ -272,16 +272,14 @@ def get_rocks( """ Fetches paginated and parsed rock packages using DeviceGW. """ - rocks2 = device_gw.find("%", fields=FIND_FIELDS).get("results", []) + rocks = device_gw.find("%", fields=FIND_FIELDS).get("results", []) - total_items = len(rocks2) + total_items = len(rocks) total_pages = (total_items + size - 1) // size page = int(query_params.get("page", 1)) - rocks_per_page = paginate(rocks2, page, size) - parsed_rocks = [] + rocks_per_page = paginate(rocks, page, size) + parsed_rocks = [parse_package_for_card(rock) for rock in rocks_per_page] - for rock in rocks_per_page: - parsed_rocks.append(parse_package_for_card(rock)) return { "packages": parsed_rocks, "total_pages": total_pages, @@ -296,7 +294,5 @@ def get_rock( Retrieves a specific rock package by its name. """ rock = device_gw.get_item_details(entity_name, fields=DETAILS_FIELDS) - if not rock: - return {} return rock From 5795b9f753e4c0d9d87e285d3ea9ce84b38c1021 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 20 Jun 2025 21:28:27 +0000 Subject: [PATCH 21/24] chore(deps): update internal dependencies --- package.json | 2 +- requirements.txt | 2 +- yarn.lock | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 3c584003..2949e527 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "dependencies": { "@canonical/cookie-policy": "3.6.5", "@canonical/global-nav": "3.6.4", - "@canonical/react-components": "2.7.1", + "@canonical/react-components": "2.7.4", "@canonical/store-components": "0.53.0", "autoprefixer": "10.4.21", "date-fns": "4.1.0", diff --git a/requirements.txt b/requirements.txt index b1e6a476..7b53e385 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -canonicalwebteam.flask-base==2.5.0 +canonicalwebteam.flask-base==2.6.0 beautifulsoup4==4.12.3 canonicalwebteam.candid==0.9.0 canonicalwebteam.discourse==6.2.0 diff --git a/yarn.lock b/yarn.lock index 25273733..953a554f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1053,10 +1053,10 @@ react-table "7.8.0" react-useportal "1.0.19" -"@canonical/react-components@2.7.1": - version "2.7.1" - resolved "https://registry.yarnpkg.com/@canonical/react-components/-/react-components-2.7.1.tgz#5a5bb5bcb8c11bad7e0f0a11ea2261d5ce335a72" - integrity sha512-OEOsVGJiitn63SLOQ8qgRrPPTzLzPS2TMAtAKvDsYMucyHttrycVyYLgCI0mDyxSIwvWm6eUnpeZvjckq/jixA== +"@canonical/react-components@2.7.4": + version "2.7.4" + resolved "https://registry.yarnpkg.com/@canonical/react-components/-/react-components-2.7.4.tgz#4c561c2e77af66d31b159e05365c3792779dbed3" + integrity sha512-RCylRIjPEuQFkhzb0c8KqCGG82GSLZ+JyPxPTMsJDLG5MvS/zoTKv2GE+cu/n7cp6ZGIpABiXVT2GD089e/+rA== dependencies: "@types/jest" "29.5.14" "@types/node" "20.17.19" From 90f2850089325b7656af4b3fdc92536b871823df Mon Sep 17 00:00:00 2001 From: codeEmpress1 Date: Thu, 26 Jun 2025 21:43:23 +0300 Subject: [PATCH 22/24] add cache to API call for rock find endpoint --- webapp/app.py | 5 +++-- webapp/extensions.py | 2 ++ webapp/store/logic.py | 14 ++++++++++---- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/webapp/app.py b/webapp/app.py index 9fd0b583..8499b7e4 100644 --- a/webapp/app.py +++ b/webapp/app.py @@ -2,7 +2,7 @@ import talisker.requests from flask import render_template, make_response, request, escape -from webapp.extensions import csrf +from webapp.extensions import csrf, cache from webapp.config import APP_NAME from webapp.handlers import set_handlers from webapp.store.views import store @@ -22,7 +22,8 @@ app.name = APP_NAME - +app.config["CACHE_TYPE"] = "simple" +cache.init_app(app) set_handlers(app) request_session = talisker.requests.get_session() diff --git a/webapp/extensions.py b/webapp/extensions.py index 99be8892..c61984c0 100644 --- a/webapp/extensions.py +++ b/webapp/extensions.py @@ -1,4 +1,6 @@ from flask_wtf.csrf import CSRFProtect +from flask_caching import Cache +cache = Cache(config={"CACHE_TYPE": "simple"}) csrf = CSRFProtect() diff --git a/webapp/store/logic.py b/webapp/store/logic.py index 3d7f61ad..5b66853b 100644 --- a/webapp/store/logic.py +++ b/webapp/store/logic.py @@ -6,6 +6,7 @@ from canonicalwebteam.store_api.devicegw import DeviceGW from webapp.helpers import get_yaml_loader +from webapp.extensions import cache ARCHITECTURES = ["amd64", "arm64", "ppc64el", "riscv64", "s390x"] FIND_FIELDS = [ @@ -155,7 +156,6 @@ def parse_package_for_card( "contact": "", }, "publisher": {"display_name": "", "name": "", "validation": ""}, - "ratings": {"value": "0", "count": "0"}, } metadata = package.get("metadata", {}) @@ -184,7 +184,7 @@ def paginate(packages: List[Packages], page: int, size: int) -> List[Packages]: Paginate the given packages list based on current page and size. """ total_items = len(packages) - total_pages = (total_items + size - 1) // size # ceiling division + total_pages = (total_items + size - 1) // size if page > total_pages: page = total_pages @@ -264,6 +264,13 @@ def parse_rock_details(rock): ) return parsed_rock +def fetch_rocks(): + cached_rocks = cache.get("cached_rocks") + if cached_rocks is not None: + return cached_rocks + rocks = device_gw.find("%", fields=FIND_FIELDS).get("results", []) + cached_rocks = cache.set("cached_rocks", rocks) + return rocks def get_rocks( size: int = 10, @@ -272,8 +279,7 @@ def get_rocks( """ Fetches paginated and parsed rock packages using DeviceGW. """ - rocks = device_gw.find("%", fields=FIND_FIELDS).get("results", []) - + rocks = fetch_rocks() total_items = len(rocks) total_pages = (total_items + size - 1) // size page = int(query_params.get("page", 1)) From 5b16754886a92b93726fa0f54de4f82d5bb2c22d Mon Sep 17 00:00:00 2001 From: codeEmpress1 Date: Thu, 26 Jun 2025 22:04:43 +0300 Subject: [PATCH 23/24] fix lint --- webapp/store/logic.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/webapp/store/logic.py b/webapp/store/logic.py index 5b66853b..f8c67e90 100644 --- a/webapp/store/logic.py +++ b/webapp/store/logic.py @@ -264,6 +264,7 @@ def parse_rock_details(rock): ) return parsed_rock + def fetch_rocks(): cached_rocks = cache.get("cached_rocks") if cached_rocks is not None: @@ -272,6 +273,7 @@ def fetch_rocks(): cached_rocks = cache.set("cached_rocks", rocks) return rocks + def get_rocks( size: int = 10, query_params: Dict[str, Any] = {}, From b413e97712e9fae15e981757f68d896ed4a9b237 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Jun 2025 13:19:40 +0000 Subject: [PATCH 24/24] chore(deps): update internal dependencies --- package.json | 2 +- requirements.txt | 4 ++-- yarn.lock | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 2949e527..67deddd2 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "@canonical/cookie-policy": "3.6.5", "@canonical/global-nav": "3.6.4", "@canonical/react-components": "2.7.4", - "@canonical/store-components": "0.53.0", + "@canonical/store-components": "0.54.0", "autoprefixer": "10.4.21", "date-fns": "4.1.0", "formik": "^2.4.5", diff --git a/requirements.txt b/requirements.txt index 7b53e385..a658a6d8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,8 +2,8 @@ canonicalwebteam.flask-base==2.6.0 beautifulsoup4==4.12.3 canonicalwebteam.candid==0.9.0 canonicalwebteam.discourse==6.2.0 -canonicalwebteam.image-template==1.5.0 -canonicalwebteam.store-api==6.3.0 +canonicalwebteam.image-template==1.6.0 +canonicalwebteam.store-api==6.4.0 canonicalwebteam.docstring-extractor==1.2.0 Flask-WTF==1.2.2 diff --git a/yarn.lock b/yarn.lock index 953a554f..38f6a854 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1069,10 +1069,10 @@ prop-types "15.8.1" react-table "7.8.0" -"@canonical/store-components@0.53.0": - version "0.53.0" - resolved "https://registry.npmjs.org/@canonical/store-components/-/store-components-0.53.0.tgz" - integrity sha512-2rSkfn5aIQAhMuzJyE3eg8dQptnk2/1oLaUg/wCpk0UtnWy6jISAZ1sLYSr3AFIx2z1nqG+7VbTG6FbjcFFIDQ== +"@canonical/store-components@0.54.0": + version "0.54.0" + resolved "https://registry.yarnpkg.com/@canonical/store-components/-/store-components-0.54.0.tgz#192aad0e75baff30bfa62accf40c90a0cb74f8a5" + integrity sha512-crYyhqfv+O+BP0yZkG8WnUkIhazpeWXfIpaAB48jQgEgksC7ARbwk4c80qlpz+44XqLptDLv/fJPQrbrIibLlg== dependencies: "@canonical/react-components" "1.9.0" "@types/jest" "27.5.2"