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

View File

@ -3,6 +3,7 @@
Properties: Properties:
- `clusters [String]`: List of cluster names - `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 - `links [Object]`: Pre-filtered link objects based on user auth
- `direction String?`: The direcion of the drop-down menue [default: down] - `direction String?`: The direcion of the drop-down menue [default: down]
--> -->
@ -18,6 +19,7 @@
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
export let clusters; export let clusters;
export let subClusters;
export let links; export let links;
export let direction = "down"; export let direction = "down";
</script> </script>
@ -47,6 +49,13 @@
> >
Node List Node List
</DropdownItem> </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> </DropdownMenu>
</Dropdown> </Dropdown>
{/each} {/each}

View File

@ -10,15 +10,16 @@
--> -->
<script> <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 { 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 NodeListRow from "./nodelist/NodeListRow.svelte";
import Pagination from "../generic/joblist/Pagination.svelte"; import Pagination from "../generic/joblist/Pagination.svelte";
export let cluster; export let cluster;
export let subCluster = ""; export let subCluster = "";
export const ccconfig = null; export let ccconfig = null;
export let selectedMetrics = []; export let selectedMetrics = [];
export let selectedResolution = 0; export let selectedResolution = 0;
export let hostnameFilter = ""; export let hostnameFilter = "";
@ -26,8 +27,9 @@
export let from = null; export let from = null;
export let to = null; export let to = null;
// let usePaging = ccconfig.node_list_usePaging // Decouple from Job List Paging Params?
let itemsPerPage = 10 // usePaging ? ccconfig.node_list_jobsPerPage : 10; let usePaging = ccconfig.job_list_usePaging
let itemsPerPage = usePaging ? ccconfig.plot_list_jobsPerPage : 10;
let page = 1; let page = 1;
let paging = { itemsPerPage, page }; let paging = { itemsPerPage, page };
@ -37,7 +39,8 @@
(x) => (headerPaddingTop = x), (x) => (headerPaddingTop = x),
); );
const { query: initq } = init(); // const { query: initq } = init();
const initialized = getContext("initialized");
const client = getContextClient(); const client = getContextClient();
const nodeListQuery = gql` const nodeListQuery = gql`
query ($cluster: String!, $subCluster: String!, $nodeFilter: String!, $metrics: [String!], $scopes: [MetricScope!]!, $from: Time!, $to: Time!, $paging: PageRequest!, $selectedResolution: Int) { 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({ $: nodesQuery = queryStore({
client: client, client: client,
query: nodeListQuery, query: nodeListQuery,
@ -105,25 +152,19 @@
requestPolicy: "network-only", // Resolution queries are cached, but how to access them? For now: reload on every change requestPolicy: "network-only", // Resolution queries are cached, but how to access them? For now: reload on every change
}); });
$: matchedNodes = $nodesQuery.data?.nodeMetricsList.totalNodes || 0; let nodes = [];
$: orderedData = $nodesQuery.data?.nodeMetricsList.items.sort((a, b) => a.host.localeCompare(b.host)); $: 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> </script>
{#if $nodesQuery.error} <Row>
<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"> <div class="col cc-table-wrapper">
<Table cellspacing="0px" cellpadding="0px"> <Table cellspacing="0px" cellpadding="0px">
<thead> <thead>
@ -148,33 +189,49 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each orderedData as nodeData (nodeData.host)} {#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}/> <NodeListRow {nodeData} {cluster} {selectedMetrics}/>
{:else} {:else}
<tr> <tr>
<td>No nodes found </td> <td colspan={selectedMetrics.length + 1}> No nodes found </td>
</tr> </tr>
{/each} {/each}
{/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> </tbody>
</Table> </Table>
</div> </div>
</Row> </Row>
{/if}
{#if true} <!-- usePaging --> {#if usePaging}
<Pagination <Pagination
bind:page bind:page
{itemsPerPage} {itemsPerPage}
itemText="Nodes" itemText="Nodes"
totalItems={matchedNodes} totalItems={matchedNodes}
on:update-paging={({ detail }) => { on:update-paging={({ detail }) => {
paging = { itemsPerPage: detail.itemsPerPage, page: detail.page } if (detail.itemsPerPage != itemsPerPage) {
// if (detail.itemsPerPage != itemsPerPage) { updateConfiguration(detail.itemsPerPage.toString(), detail.page);
// updateConfiguration(detail.itemsPerPage.toString(), detail.page); } else {
// } else { nodes = []
// // nodes = [] paging = { itemsPerPage: detail.itemsPerPage, page: detail.page };
// paging = { itemsPerPage: detail.itemsPerPage, page: detail.page }; }
// }
}} }}
/> />
{/if} {/if}

View File

@ -18,6 +18,7 @@
"username": "{{ .User.Username }}", "username": "{{ .User.Username }}",
"authlevel": {{ .User.GetAuthLevel }}, "authlevel": {{ .User.GetAuthLevel }},
"clusters": {{ .Clusters }}, "clusters": {{ .Clusters }},
"subClusters": {{ .SubClusters }},
"roles": {{ .Roles }} "roles": {{ .Roles }}
}; };
</script> </script>

View File

@ -13,6 +13,7 @@ import (
"github.com/ClusterCockpit/cc-backend/internal/config" "github.com/ClusterCockpit/cc-backend/internal/config"
"github.com/ClusterCockpit/cc-backend/internal/util" "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/log"
"github.com/ClusterCockpit/cc-backend/pkg/schema" "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 Roles map[string]schema.Role // Available roles for frontend render checks
Build Build // Latest information about the application Build Build // Latest information about the application
Clusters []schema.ClusterConfig // List of all clusters for use in the Header 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. 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>) 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, ...) 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) log.Debugf("Page config : %v\n", page.Config)
if err := t.Execute(rw, page); err != nil { if err := t.Execute(rw, page); err != nil {
log.Errorf("Template error: %s", err.Error()) log.Errorf("Template error: %s", err.Error())