mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-01-23 18:09:06 +01:00
add navbar select, add continous scroll, paging persistance
This commit is contained in:
parent
5ea11a5ad2
commit
e55798944e
@ -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',
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
11
web/web.go
11
web/web.go
@ -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())
|
||||
|
Loading…
Reference in New Issue
Block a user