Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

doc: boards: Add ability to filter by SoC in the board catalog #79670

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions boards/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,20 @@ Shields are hardware add-ons that can be stacked on top of a board to add extra
functionality. They are listed separately from boards, towards :ref:`the end of
this page <boards-shields>`.

Use the interactive search form below to quickly navigate through the list of
supported boards.
.. admonition:: Search Tips
:class: dropdown

* Use the form below to filter the list of supported boards. If a field is left empty, it will
not be used in the filtering process.

* A board must meet **all** criteria selected across different fields. For example, if you select
both a vendor and an architecture, only boards that match both will be displayed. Within a
single field, selecting multiple options (such as two architectures) will show boards matching
**either** option.

* Can't find your exact board? Don't worry! If a similar board with the same or a closely related
MCU exists, you can use it as a :ref:`starting point <create-your-board-directory>` for adding
support for your own board.

.. toctree::
:maxdepth: 2
Expand Down
13 changes: 13 additions & 0 deletions doc/_extensions/zephyr/domain/static/css/board-catalog.css
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,19 @@
margin-bottom: 5px;
}

.filter-form #family, .filter-form #series, .filter-form #soc {
overflow: auto;
border-radius: 8px;
padding-right: 18px;
}

.filter-form #family option:checked,
.filter-form #series option:checked,
.filter-form #soc option:checked {
background-color: var(--admonition-note-title-background-color);
color: var(--admonition-note-title-color);
}

.select-container {
position: relative;
}
Expand Down
113 changes: 98 additions & 15 deletions doc/_extensions/zephyr/domain/static/js/board-catalog.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,30 +14,76 @@ function toggleDisplayMode(btn) {
}

function populateFormFromURL() {
const params = ["name", "arch", "vendor"];
const params = ["name", "arch", "vendor", "soc"];
const hashParams = new URLSearchParams(window.location.hash.slice(1));
params.forEach((param) => {
const element = document.getElementById(param);
if (hashParams.has(param)) {
element.value = hashParams.get(param);
const value = hashParams.get(param);
if (param === "soc") {
value.split(",").forEach(soc =>
element.querySelector(`option[value="${soc}"]`).selected = true);
} else {
element.value = value;
}
}
});

filterBoards();
}

function updateURL() {
const params = ["name", "arch", "vendor"];
const params = ["name", "arch", "vendor", "soc"];
const hashParams = new URLSearchParams(window.location.hash.slice(1));

params.forEach((param) => {
const value = document.getElementById(param).value;
value ? hashParams.set(param, value) : hashParams.delete(param);
const element = document.getElementById(param);
if (param === "soc") {
const selectedSocs = [...element.selectedOptions].map(({ value }) => value);
selectedSocs.length ? hashParams.set(param, selectedSocs.join(",")) : hashParams.delete(param);
}
else {
element.value ? hashParams.set(param, element.value) : hashParams.delete(param);
}
});

window.history.replaceState({}, "", `#${hashParams.toString()}`);
}

function fillSocFamilySelect() {
const socFamilySelect = document.getElementById("family");

Object.keys(socs_data).sort().forEach(f => {
socFamilySelect.add(new Option(f));
});
}

function fillSocSeriesSelect(families, selectOnFill = false) {
const socSeriesSelect = document.getElementById("series");

families = families?.length ? families : Object.keys(socs_data);
let allSeries = [...new Set(families.flatMap(f => Object.keys(socs_data[f])))];

socSeriesSelect.innerHTML = "";
allSeries.sort().map(s => {
const option = new Option(s, s, selectOnFill, selectOnFill);
socSeriesSelect.add(option);
});
}

function fillSocSocSelect(families, series = undefined, selectOnFill = false) {
const socSocSelect = document.getElementById("soc");

families = families?.length ? families : Object.keys(socs_data);
series = series?.length ? series : families.flatMap(f => Object.keys(socs_data[f]));
matchingSocs = [...new Set(families.flatMap(f => series.flatMap(s => socs_data[f][s] || [])))];

socSocSelect.innerHTML = "";
matchingSocs.sort().forEach((soc) => {
socSocSelect.add(new Option(soc, soc, selectOnFill, selectOnFill));
});
}

document.addEventListener("DOMContentLoaded", function () {
const form = document.querySelector(".filter-form");

Expand All @@ -52,18 +98,53 @@ document.addEventListener("DOMContentLoaded", function () {
vendorSelect.appendChild(option);
});

fillSocFamilySelect();
fillSocSeriesSelect();
fillSocSocSelect();

populateFormFromURL();

form.addEventListener("submit", function (event) {
event.preventDefault();
socFamilySelect = document.getElementById("family");
socFamilySelect.addEventListener("change", () => {
const selectedFamilies = [...socFamilySelect.selectedOptions].map(({ value }) => value);
fillSocSeriesSelect(selectedFamilies, true);
fillSocSocSelect(selectedFamilies, undefined, true);
filterBoards();
});

socSeriesSelect = document.getElementById("series");
socSeriesSelect.addEventListener("change", () => {
const selectedFamilies = [...socFamilySelect.selectedOptions].map(({ value }) => value);
const selectedSeries = [...socSeriesSelect.selectedOptions].map(({ value }) => value);
fillSocSocSelect(selectedFamilies, selectedSeries, true);
filterBoards();
});

socSocSelect = document.getElementById("soc");
socSocSelect.addEventListener("change", () => {
filterBoards();
});

form.addEventListener("input", function () {
filterBoards();
updateURL();
});

form.addEventListener("submit", function (event) {
event.preventDefault();
});

filterBoards();
});

function resetForm() {
const form = document.querySelector(".filter-form");
form.reset();
fillSocFamilySelect();
fillSocSeriesSelect();
fillSocSocSelect();
filterBoards();
}

function updateBoardCount() {
const boards = document.getElementsByClassName("board-card");
const visibleBoards = Array.from(boards).filter(
Expand All @@ -77,9 +158,10 @@ function filterBoards() {
const nameInput = document.getElementById("name").value.toLowerCase();
const archSelect = document.getElementById("arch").value;
const vendorSelect = document.getElementById("vendor").value;
const socSocSelect = document.getElementById("soc");

const resetFiltersBtn = document.getElementById("reset-filters");
if (nameInput || archSelect || vendorSelect) {
if (nameInput || archSelect || vendorSelect || socSocSelect.selectedOptions.length) {
resetFiltersBtn.classList.remove("btn-disabled");
} else {
resetFiltersBtn.classList.add("btn-disabled");
Expand All @@ -91,20 +173,21 @@ function filterBoards() {
const boardName = board.getAttribute("data-name").toLowerCase();
const boardArchs = board.getAttribute("data-arch").split(" ");
const boardVendor = board.getAttribute("data-vendor");
const boardSocs = board.getAttribute("data-socs").split(" ");

let matches = true;

const selectedSocs = [...socSocSelect.selectedOptions].map(({ value }) => value);

matches =
!(nameInput && !boardName.includes(nameInput)) &&
!(archSelect && !boardArchs.includes(archSelect)) &&
!(vendorSelect && boardVendor !== vendorSelect);
!(vendorSelect && boardVendor !== vendorSelect) &&
(selectedSocs.length === 0 || selectedSocs.some((soc) => boardSocs.includes(soc)));

if (matches) {
board.classList.remove("hidden");
} else {
board.classList.add("hidden");
}
board.classList.toggle("hidden", !matches);
});

updateURL();
updateBoardCount();
}
1 change: 1 addition & 0 deletions doc/_extensions/zephyr/domain/templates/board-card.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
data-name="{{ board.full_name}}"
data-arch="{{ board.archs | join(" ") }}"
data-vendor="{{ board.vendor }}"
data-socs="{{ board.socs | join(" ") }}"
tabindex="0">
<div class="vendor">{{ catalog.vendors[board.vendor] }}</div>
{% if board.image -%}
Expand Down
21 changes: 20 additions & 1 deletion doc/_extensions/zephyr/domain/templates/board-catalog.html
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,29 @@
</div>
</div>

<div class="form-group">
<label for="family">Family</label>
<select id="family" name="family" size="10" multiple></select>
</div>

<div class="form-group">
<label for="series">Series</label>
<select id="series" name="series" size="10" multiple></select>
</div>

<div class="form-group">
<label for="soc">SoC</label>
<select id="soc" name="soc" size="10" multiple></select>
</div>

</form>

<div id="form-options" style="text-align: center; margin-bottom: 20px">
<button
id="reset-filters"
class="btn btn-info btn-disabled fa fa-times"
tabindex="0"
onclick="document.querySelector('.filter-form').reset(); filterBoards(); updateURL();">
onclick="resetForm()">
Reset Filters
</button>
<button
Expand All @@ -71,3 +86,7 @@
{% include "board-card.html" %}
{% endfor %}
</div>

<script>
socs_data = {{ catalog.socs | tojson }};
</script>
30 changes: 13 additions & 17 deletions doc/_scripts/gen_boards_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from collections import namedtuple
from pathlib import Path

import list_boards
import list_boards, list_hardware
import pykwalify
import yaml
import zephyr_module
Expand All @@ -15,6 +15,7 @@

logger = logging.getLogger(__name__)


def guess_file_from_patterns(directory, patterns, name, extensions):
for pattern in patterns:
for ext in extensions:
Expand Down Expand Up @@ -77,6 +78,7 @@ def get_catalog():
)

boards = list_boards.find_v2_boards(args_find_boards)
systems = list_hardware.find_v2_systems(args_find_boards)
board_catalog = {}

for board in boards:
Expand All @@ -99,29 +101,23 @@ def get_catalog():
except Exception as e:
logger.error(f"Error parsing twister file {twister_file}: {e}")

full_name = board.full_name
socs = {soc.name for soc in board.socs}
full_name = board.full_name or board.name
doc_page = guess_doc_page(board)

if not full_name:
# If full commercial name of the board is not available through board.full_name, we look
# for the title in the board's documentation page.
# /!\ This is a temporary solution until #79571 sets all the full names in the boards
if doc_page:
with open(doc_page, "r") as f:
lines = f.readlines()
for i, line in enumerate(lines):
if line.startswith("#"):
full_name = lines[i - 1].strip()
break
else:
full_name = board.name

board_catalog[board.name] = {
"full_name": full_name,
"doc_page": doc_page.relative_to(ZEPHYR_BASE).as_posix() if doc_page else None,
"vendor": vendor,
"archs": list(archs),
"socs": list(socs),
"image": guess_image(board),
}

return {"boards": board_catalog, "vendors": vnd_lookup.vnd2vendor}
socs_hierarchy = {}
for soc in systems.get_socs():
family = soc.family or "<no family>"
series = soc.series or "<no series>"
socs_hierarchy.setdefault(family, {}).setdefault(series, []).append(soc.name)

return {"boards": board_catalog, "vendors": vnd_lookup.vnd2vendor, "socs": socs_hierarchy}
Loading