add navbar select, add continous scroll, paging persistance

This commit is contained in:
Christoph Kluge 2025-01-10 18:02:54 +01:00
parent 5ea11a5ad2
commit e55798944e
5 changed files with 150 additions and 64 deletions

View File

@ -26,6 +26,7 @@
export let username;
export let authlevel;
export let clusters;
export let subClusters;
export let roles;
let isOpen = false;
@ -138,11 +139,13 @@
{#if screenSize > 1500 || screenSize < 768}
<NavbarLinks
{clusters}
{subClusters}
links={views.filter((item) => item.requiredRole <= authlevel)}
/>
{:else if screenSize > 1300}
<NavbarLinks
{clusters}
{subClusters}
links={views.filter(
(item) => item.requiredRole <= authlevel && item.menu != "Info",
)}
@ -156,6 +159,7 @@
<DropdownMenu class="dropdown-menu-lg-end">
<NavbarLinks
{clusters}
{subClusters}
direction="right"
links={views.filter(
(item) =>
@ -168,6 +172,7 @@
{:else}
<NavbarLinks
{clusters}
{subClusters}
links={views.filter(
(item) => item.requiredRole <= authlevel && item.menu == "none",
)}
@ -180,6 +185,7 @@
<DropdownMenu class="dropdown-menu-lg-end">
<NavbarLinks
{clusters}
{subClusters}
direction="right"
links={views.filter(
(item) => item.requiredRole <= authlevel && item.menu == 'Jobs',
@ -196,6 +202,7 @@
<DropdownMenu class="dropdown-menu-lg-end">
<NavbarLinks
{clusters}
{subClusters}
direction="right"
links={views.filter(
(item) => item.requiredRole <= authlevel && item.menu == 'Groups',
@ -212,6 +219,7 @@
<DropdownMenu class="dropdown-menu-lg-end">
<NavbarLinks
{clusters}
{subClusters}
direction="right"
links={views.filter(
(item) => item.requiredRole <= authlevel && item.menu == 'Info',

View File

@ -3,6 +3,7 @@
Properties:
- `clusters [String]`: List of cluster names
- `subClusters map[String][]string`: Map of subclusters by cluster names
- `links [Object]`: Pre-filtered link objects based on user auth
- `direction String?`: The direcion of the drop-down menue [default: down]
-->
@ -18,6 +19,7 @@
} from "@sveltestrap/sveltestrap";
export let clusters;
export let subClusters;
export let links;
export let direction = "down";
</script>
@ -47,6 +49,13 @@
>
Node List
</DropdownItem>
{#each subClusters[cluster.name] as subCluster}
<DropdownItem class="py-1 px-2"
href={item.href + 'list/' + cluster.name + '/' + subCluster}
>
{subCluster} Node List
</DropdownItem>
{/each}
</DropdownMenu>
</Dropdown>
{/each}

View File

@ -10,15 +10,16 @@
-->
<script>
import { queryStore, gql, getContextClient } from "@urql/svelte";
import { getContext } from "svelte";
import { queryStore, gql, getContextClient, mutationStore } from "@urql/svelte";
import { Row, Col, Card, Table, Spinner } from "@sveltestrap/sveltestrap";
import { init, stickyHeader } from "../generic/utils.js";
import { stickyHeader } from "../generic/utils.js";
import NodeListRow from "./nodelist/NodeListRow.svelte";
import Pagination from "../generic/joblist/Pagination.svelte";
export let cluster;
export let subCluster = "";
export const ccconfig = null;
export let ccconfig = null;
export let selectedMetrics = [];
export let selectedResolution = 0;
export let hostnameFilter = "";
@ -26,8 +27,9 @@
export let from = null;
export let to = null;
// let usePaging = ccconfig.node_list_usePaging
let itemsPerPage = 10 // usePaging ? ccconfig.node_list_jobsPerPage : 10;
// Decouple from Job List Paging Params?
let usePaging = ccconfig.job_list_usePaging
let itemsPerPage = usePaging ? ccconfig.plot_list_jobsPerPage : 10;
let page = 1;
let paging = { itemsPerPage, page };
@ -37,7 +39,8 @@
(x) => (headerPaddingTop = x),
);
const { query: initq } = init();
// const { query: initq } = init();
const initialized = getContext("initialized");
const client = getContextClient();
const nodeListQuery = gql`
query ($cluster: String!, $subCluster: String!, $nodeFilter: String!, $metrics: [String!], $scopes: [MetricScope!]!, $from: Time!, $to: Time!, $paging: PageRequest!, $selectedResolution: Int) {
@ -88,6 +91,50 @@
}
`
const updateConfigurationMutation = ({ name, value }) => {
return mutationStore({
client: client,
query: gql`
mutation ($name: String!, $value: String!) {
updateConfiguration(name: $name, value: $value)
}
`,
variables: { name, value },
});
};
// Decouple from Job List Paging Params?
function updateConfiguration(value, page) {
updateConfigurationMutation({
name: "plot_list_jobsPerPage",
value: value,
}).subscribe((res) => {
if (res.fetching === false && !res.error) {
nodes = [] // Empty List
paging = { itemsPerPage: value, page: page }; // Trigger reload of nodeList
} else if (res.fetching === false && res.error) {
throw res.error;
}
});
}
if (!usePaging) {
window.addEventListener('scroll', () => {
let {
scrollTop,
scrollHeight,
clientHeight
} = document.documentElement;
// Add 100 px offset to trigger load earlier
if (scrollTop + clientHeight >= scrollHeight - 100 && $nodesQuery?.data != null && $nodesQuery.data?.nodeMetricsList.hasNextPage) {
let pendingPaging = { ...paging }
pendingPaging.page += 1
paging = pendingPaging
};
});
};
$: nodesQuery = queryStore({
client: client,
query: nodeListQuery,
@ -105,76 +152,86 @@
requestPolicy: "network-only", // Resolution queries are cached, but how to access them? For now: reload on every change
});
$: matchedNodes = $nodesQuery.data?.nodeMetricsList.totalNodes || 0;
$: orderedData = $nodesQuery.data?.nodeMetricsList.items.sort((a, b) => a.host.localeCompare(b.host));
let nodes = [];
$: if ($initialized && $nodesQuery.data) {
if (usePaging) {
nodes = [...$nodesQuery.data.nodeMetricsList.items].sort((a, b) => a.host.localeCompare(b.host));
} else {
nodes = nodes.concat([...$nodesQuery.data.nodeMetricsList.items].sort((a, b) => a.host.localeCompare(b.host)))
}
}
$: matchedNodes = $nodesQuery.data?.nodeMetricsList.totalNodes || matchedNodes;
</script>
{#if $nodesQuery.error}
<Row>
<Col>
<Card body color="danger">{$nodesQuery.error.message}</Card>
</Col>
</Row>
{:else if $nodesQuery.fetching }
<Row>
<Col>
<Spinner />
</Col>
</Row>
{:else if $initq?.data && $nodesQuery?.data}
<Row>
<div class="col cc-table-wrapper">
<Table cellspacing="0px" cellpadding="0px">
<thead>
<tr>
<th
class="position-sticky top-0 text-capitalize"
scope="col"
style="padding-top: {headerPaddingTop}px;"
>
{cluster} Node Info
</th>
<Row>
<div class="col cc-table-wrapper">
<Table cellspacing="0px" cellpadding="0px">
<thead>
<tr>
<th
class="position-sticky top-0 text-capitalize"
scope="col"
style="padding-top: {headerPaddingTop}px;"
>
{cluster} Node Info
</th>
{#each selectedMetrics as metric (metric)}
<th
class="position-sticky top-0 text-center"
scope="col"
style="padding-top: {headerPaddingTop}px"
>
{metric} ({systemUnits[metric]})
</th>
{/each}
</tr>
</thead>
<tbody>
{#each orderedData as nodeData (nodeData.host)}
{#each selectedMetrics as metric (metric)}
<th
class="position-sticky top-0 text-center"
scope="col"
style="padding-top: {headerPaddingTop}px"
>
{metric} ({systemUnits[metric]})
</th>
{/each}
</tr>
</thead>
<tbody>
{#if $nodesQuery.error}
<Row>
<Col>
<Card body color="danger">{$nodesQuery.error.message}</Card>
</Col>
</Row>
{:else}
{#each nodes as nodeData (nodeData.host)}
<NodeListRow {nodeData} {cluster} {selectedMetrics}/>
{:else}
<tr>
<td>No nodes found </td>
<td colspan={selectedMetrics.length + 1}> No nodes found </td>
</tr>
{/each}
</tbody>
</Table>
</div>
</Row>
{/if}
{/if}
{#if $nodesQuery.fetching || !$nodesQuery.data}
<tr>
<td colspan={selectedMetrics.length + 1}>
<div style="text-align:center;">
<p><b>Loading nodes {nodes.length + 1} to {nodes.length + paging.itemsPerPage} {matchedNodes ? `of ${matchedNodes} total` : ``}</b></p>
<Spinner secondary />
</div>
</td>
</tr>
{/if}
</tbody>
</Table>
</div>
</Row>
{#if true} <!-- usePaging -->
{#if usePaging}
<Pagination
bind:page
{itemsPerPage}
itemText="Nodes"
totalItems={matchedNodes}
on:update-paging={({ detail }) => {
paging = { itemsPerPage: detail.itemsPerPage, page: detail.page }
// if (detail.itemsPerPage != itemsPerPage) {
// updateConfiguration(detail.itemsPerPage.toString(), detail.page);
// } else {
// // nodes = []
// paging = { itemsPerPage: detail.itemsPerPage, page: detail.page };
// }
if (detail.itemsPerPage != itemsPerPage) {
updateConfiguration(detail.itemsPerPage.toString(), detail.page);
} else {
nodes = []
paging = { itemsPerPage: detail.itemsPerPage, page: detail.page };
}
}}
/>
{/if}

View File

@ -15,10 +15,11 @@
{{end}}
<script>
const header = {
"username": "{{ .User.Username }}",
"authlevel": {{ .User.GetAuthLevel }},
"clusters": {{ .Clusters }},
"roles": {{ .Roles }}
"username": "{{ .User.Username }}",
"authlevel": {{ .User.GetAuthLevel }},
"clusters": {{ .Clusters }},
"subClusters": {{ .SubClusters }},
"roles": {{ .Roles }}
};
</script>
</head>

View File

@ -13,6 +13,7 @@ import (
"github.com/ClusterCockpit/cc-backend/internal/config"
"github.com/ClusterCockpit/cc-backend/internal/util"
"github.com/ClusterCockpit/cc-backend/pkg/archive"
"github.com/ClusterCockpit/cc-backend/pkg/log"
"github.com/ClusterCockpit/cc-backend/pkg/schema"
)
@ -95,6 +96,7 @@ type Page struct {
Roles map[string]schema.Role // Available roles for frontend render checks
Build Build // Latest information about the application
Clusters []schema.ClusterConfig // List of all clusters for use in the Header
SubClusters map[string][]string // Map per cluster of all subClusters for use in the Header
FilterPresets map[string]interface{} // For pages with the Filter component, this can be used to set initial filters.
Infos map[string]interface{} // For generic use (e.g. username for /monitoring/user/<id>, job id for /monitoring/job/<id>)
Config map[string]interface{} // UI settings for the currently logged in user (e.g. line width, ...)
@ -114,6 +116,15 @@ func RenderTemplate(rw http.ResponseWriter, file string, page *Page) {
}
}
if page.SubClusters == nil {
page.SubClusters = make(map[string][]string)
for _, cluster := range archive.Clusters {
for _, sc := range cluster.SubClusters {
page.SubClusters[cluster.Name] = append(page.SubClusters[cluster.Name], sc.Name)
}
}
}
log.Debugf("Page config : %v\n", page.Config)
if err := t.Execute(rw, page); err != nil {
log.Errorf("Template error: %s", err.Error())