Navigation refactoring, handling extension page params (#1651)

* decentralizing page url-params management -- PoC / tsc 4.1 random fixes

Signed-off-by: Roman <ixrock@gmail.com>

* fixes, tweak example-extension for demo

Signed-off-by: Roman <ixrock@gmail.com>

* lint fixes, revert tests

Signed-off-by: Roman <ixrock@gmail.com>

* removed occasional changes related to typescript 4.1

Signed-off-by: Roman <ixrock@gmail.com>

* updated example with 2 menu-items targeting same page with different params

Signed-off-by: Roman <ixrock@gmail.com>

* fix: merge page url chunks with native URL()-api, simplified default page-params registration

Signed-off-by: Roman <ixrock@gmail.com>

* fix: make lint happy

Signed-off-by: Roman <ixrock@gmail.com>

* fix: unit-tests

Signed-off-by: Roman <ixrock@gmail.com>

* renaming by jim's request: UrlParam => PageParam (type), createUrlParam => createPageParam (helper)

Signed-off-by: Roman <ixrock@gmail.com>

* fix: reverting NamespaceStore public-api breaking changes

Signed-off-by: Roman <ixrock@gmail.com>

* lint fix

Signed-off-by: Roman <ixrock@gmail.com>

* fine-tuning

Signed-off-by: Roman <ixrock@gmail.com>

* yes, lint always unhappy

Signed-off-by: Roman <ixrock@gmail.com>

* fix build

Signed-off-by: Roman <ixrock@gmail.com>

* small fixes

Signed-off-by: Roman <ixrock@gmail.com>

* fix merge-conflicts

Signed-off-by: Roman <ixrock@gmail.com>

* removed `isSystem` page-param's init field exposed to extensions-api

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman authored and GitHub committed 2020-12-22 15:29:25 +02:00
1 parent 11c611dabe
commit be4e1aa15c
64 files changed
+751 -518

No files matched your search

@@ -20,13 +20,13 @@ import { Button } from "../button";
import { releaseStore } from "./release.store";
import { Notifications } from "../notifications";
import { createUpgradeChartTab } from "../dock/upgrade-chart.store";
import { getDetailsUrl } from "../../navigation";
import { _i18n } from "../../i18n";
import { themeStore } from "../../theme.store";
import { apiManager } from "../../api/api-manager";
import { SubTitle } from "../layout/sub-title";
import { secretsStore } from "../+config-secrets/secrets.store";
import { Secret } from "../../api/endpoints";
import { getDetailsUrl } from "../kube-object";
interface Props {
release: HelmRelease;
@@ -161,10 +161,7 @@ export class ReleaseDetails extends Component<Props> {
const name = item.getName();
const namespace = item.getNs();
const api = apiManager.getApi(item.metadata.selfLink);
const detailsUrl = api ? getDetailsUrl(api.getUrl({
name,
namespace,
})) : "";
const detailsUrl = api ? getDetailsUrl(api.getUrl({ name, namespace })) : "";
return (
<TableRow key={item.getId()}>
+2 -2
View File
@@ -4,12 +4,12 @@ import { Trans } from "@lingui/macro";
import { TabLayout, TabLayoutRoute } from "../layout/tab-layout";
import { HelmCharts, helmChartsRoute, helmChartsURL } from "../+apps-helm-charts";
import { HelmReleases, releaseRoute, releaseURL } from "../+apps-releases";
import { namespaceStore } from "../+namespaces/namespace.store";
import { namespaceUrlParam } from "../+namespaces/namespace.store";
@observer
export class Apps extends React.Component {
static get tabRoutes(): TabLayoutRoute[] {
const query = namespaceStore.getContextParams();
const query = namespaceUrlParam.toObjectParam();
return [
{
@@ -10,11 +10,11 @@ import { Table, TableCell, TableHead, TableRow } from "../table";
import { nodesStore } from "../+nodes/nodes.store";
import { eventStore } from "../+events/event.store";
import { autobind, cssNames, prevDefault } from "../../utils";
import { getSelectedDetails, showDetails } from "../../navigation";
import { ItemObject } from "../../item.store";
import { Spinner } from "../spinner";
import { themeStore } from "../../theme.store";
import { lookupApiLink } from "../../api/kube-api";
import { kubeSelectedUrlParam, showDetails } from "../kube-object";
interface Props {
className?: string;
@@ -85,7 +85,7 @@ export class ClusterIssues extends React.Component<Props> {
<TableRow
key={getId()}
sortItem={warning}
selected={selfLink === getSelectedDetails()}
selected={selfLink === kubeSelectedUrlParam.get()}
onClick={prevDefault(() => showDetails(selfLink))}
>
<TableCell className="message">
@@ -5,13 +5,12 @@ import { observer } from "mobx-react";
import { Link } from "react-router-dom";
import { DrawerItem, DrawerTitle } from "../drawer";
import { Badge } from "../badge";
import { KubeObjectDetailsProps } from "../kube-object";
import { KubeObjectDetailsProps, getDetailsUrl } from "../kube-object";
import { cssNames } from "../../utils";
import { HorizontalPodAutoscaler, HpaMetricType, IHpaMetric } from "../../api/endpoints/hpa.api";
import { KubeEventDetails } from "../+events/kube-event-details";
import { Trans } from "@lingui/macro";
import { Table, TableCell, TableHead, TableRow } from "../table";
import { getDetailsUrl } from "../../navigation";
import { lookupApiLink } from "../../api/kube-api";
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
@@ -17,8 +17,8 @@ import { Icon } from "../icon";
import { IKubeObjectMetadata } from "../../api/kube-object";
import { base64 } from "../../utils";
import { Notifications } from "../notifications";
import { showDetails } from "../../navigation";
import upperFirst from "lodash/upperFirst";
import { showDetails } from "../kube-object";
interface Props extends Partial<DialogProps> {
}
+2 -2
View File
@@ -4,7 +4,7 @@ import { Trans } from "@lingui/macro";
import { TabLayout, TabLayoutRoute } from "../layout/tab-layout";
import { ConfigMaps, configMapsRoute, configMapsURL } from "../+config-maps";
import { Secrets, secretsRoute, secretsURL } from "../+config-secrets";
import { namespaceStore } from "../+namespaces/namespace.store";
import { namespaceUrlParam } from "../+namespaces/namespace.store";
import { resourceQuotaRoute, ResourceQuotas, resourceQuotaURL } from "../+config-resource-quotas";
import { pdbRoute, pdbURL, PodDisruptionBudgets } from "../+config-pod-disruption-budgets";
import { HorizontalPodAutoscalers, hpaRoute, hpaURL } from "../+config-autoscalers";
@@ -13,7 +13,7 @@ import { isAllowedResource } from "../../../common/rbac";
@observer
export class Config extends React.Component {
static get tabRoutes(): TabLayoutRoute[] {
const query = namespaceStore.getContextParams();
const query = namespaceUrlParam.toObjectParam();
const routes: TabLayoutRoute[] = [];
if (isAllowedResource("configmaps")) {
@@ -10,9 +10,16 @@ import { KubeObjectListLayout } from "../kube-object";
import { crdStore } from "./crd.store";
import { CustomResourceDefinition } from "../../api/endpoints/crd.api";
import { Select, SelectOption } from "../select";
import { navigation, setQueryParams } from "../../navigation";
import { createPageParam } from "../../navigation";
import { Icon } from "../icon";
export const crdGroupsUrlParam = createPageParam<string[]>({
name: "groups",
multiValues: true,
isSystem: true,
defaultValue: [],
});
enum sortBy {
kind = "kind",
group = "group",
@@ -23,17 +30,19 @@ enum sortBy {
@observer
export class CrdList extends React.Component {
@computed get groups() {
return navigation.searchParams.getAsArray("groups");
@computed get groups(): string[] {
return crdGroupsUrlParam.get();
}
onGroupChange(group: string) {
const groups = [...this.groups];
const index = groups.findIndex(item => item == group);
onSelectGroup(group: string) {
const groups = new Set(this.groups);
if (index !== -1) groups.splice(index, 1);
else groups.push(group);
setQueryParams({ groups });
if (groups.has(group)) {
groups.delete(group); // toggle selection
} else {
groups.add(group);
}
crdGroupsUrlParam.set(Array.from(groups));
}
render() {
@@ -71,7 +80,7 @@ export class CrdList extends React.Component {
className="group-select"
placeholder={placeholder}
options={Object.keys(crdStore.groups)}
onChange={({ value: group }: SelectOption) => this.onGroupChange(group)}
onChange={({ value: group }: SelectOption) => this.onSelectGroup(group)}
controlShouldRenderValue={false}
formatOptionLabel={({ value: group }: SelectOption) => {
const isSelected = selectedGroups.includes(group);
@@ -6,10 +6,9 @@ import { Trans } from "@lingui/macro";
import { DrawerItem, DrawerTitle } from "../drawer";
import { Link } from "react-router-dom";
import { observer } from "mobx-react";
import { KubeObjectDetailsProps } from "../kube-object";
import { KubeObjectDetailsProps, getDetailsUrl } from "../kube-object";
import { KubeEvent } from "../../api/endpoints/events.api";
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { getDetailsUrl } from "../../navigation";
import { Table, TableCell, TableHead, TableRow } from "../table";
import { lookupApiLink } from "../../api/kube-api";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
+1 -2
View File
@@ -4,14 +4,13 @@ import React, { Fragment } from "react";
import { observer } from "mobx-react";
import { TabLayout } from "../layout/tab-layout";
import { eventStore } from "./event.store";
import { KubeObjectListLayout, KubeObjectListLayoutProps } from "../kube-object";
import { KubeObjectListLayout, KubeObjectListLayoutProps, getDetailsUrl } from "../kube-object";
import { Trans } from "@lingui/macro";
import { KubeEvent } from "../../api/endpoints/events.api";
import { Tooltip } from "../tooltip";
import { Link } from "react-router-dom";
import { cssNames, IClassName, stopPropagation } from "../../utils";
import { Icon } from "../icon";
import { getDetailsUrl } from "../../navigation";
import { lookupApiLink } from "../../api/kube-api";
enum sortBy {
@@ -7,9 +7,8 @@ import { Trans } from "@lingui/macro";
import { DrawerItem } from "../drawer";
import { cssNames } from "../../utils";
import { Namespace } from "../../api/endpoints";
import { KubeObjectDetailsProps } from "../kube-object";
import { getDetailsUrl, KubeObjectDetailsProps } from "../kube-object";
import { Link } from "react-router-dom";
import { getDetailsUrl } from "../../navigation";
import { Spinner } from "../spinner";
import { resourceQuotaStore } from "../+config-resource-quotas/resource-quotas.store";
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
@@ -1,44 +1,52 @@
import { action, observable, reaction } from "mobx";
import { action, comparer, observable, reaction } from "mobx";
import { autobind, createStorage } from "../../utils";
import { KubeObjectStore } from "../../kube-object.store";
import { Namespace, namespacesApi } from "../../api/endpoints";
import { IQueryParams, navigation, setQueryParams } from "../../navigation";
import { createPageParam } from "../../navigation";
import { apiManager } from "../../api/api-manager";
import { isAllowedResource } from "../../../common/rbac";
import { getHostedCluster } from "../../../common/cluster-store";
const storage = createStorage<string[]>("context_namespaces", []);
export const namespaceUrlParam = createPageParam<string[]>({
name: "namespaces",
isSystem: true,
multiValues: true,
get defaultValue() {
return storage.get(); // initial namespaces coming from URL or local-storage (default)
}
});
@autobind()
export class NamespaceStore extends KubeObjectStore<Namespace> {
api = namespacesApi;
contextNs = observable.array<string>();
protected storage = createStorage<string[]>("context_ns", this.contextNs);
get initNamespaces() {
const fromUrl = navigation.searchParams.getAsArray("namespaces");
return fromUrl.length ? fromUrl : this.storage.get();
}
constructor() {
super();
this.init();
}
// restore context namespaces
const { initNamespaces: namespaces } = this;
private init() {
this.setContext(this.initNamespaces);
this.setContext(namespaces);
this.updateUrl(namespaces);
// sync with local-storage & url-search-params
reaction(() => this.contextNs.toJS(), namespaces => {
this.storage.set(namespaces);
this.updateUrl(namespaces);
return reaction(() => this.contextNs.toJS(), namespaces => {
storage.set(namespaces); // save to local-storage
namespaceUrlParam.set(namespaces, { replaceHistory: true }); // update url
}, {
fireImmediately: true,
equals: comparer.identity,
});
}
getContextParams(): Partial<IQueryParams> {
get initNamespaces() {
return namespaceUrlParam.get();
}
getContextParams() {
return {
namespaces: this.contextNs
namespaces: this.contextNs.toJS(),
};
}
@@ -47,16 +55,12 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
// if user has given static list of namespaces let's not start watches because watch adds stuff that's not wanted
if (accessibleNamespaces.length > 0) {
return () => { return; };
return Function; // no-op
}
return super.subscribe(apis);
}
protected updateUrl(namespaces: string[]) {
setQueryParams({ namespaces }, { replace: true });
}
protected async loadItems(namespaces?: string[]) {
if (!isAllowedResource("namespaces")) {
if (namespaces) return namespaces.map(this.getDummyNamespace);
@@ -84,6 +88,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
});
}
@action
setContext(namespaces: string[]) {
this.contextNs.replace(namespaces);
}
@@ -94,6 +99,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
return context.every(namespace => this.contextNs.includes(namespace));
}
@action
toggleContext(namespace: string) {
if (this.hasContext(namespace)) this.contextNs.remove(namespace);
else this.contextNs.push(namespace);
@@ -105,6 +111,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
this.contextNs.clear();
}
@action
async remove(item: Namespace) {
await super.remove(item);
this.contextNs.remove(item.getName());
@@ -7,8 +7,8 @@ import { Trans } from "@lingui/macro";
import { Table, TableCell, TableHead, TableRow } from "../table";
import { autobind } from "../../utils";
import { lookupApiLink } from "../../api/kube-api";
import { getDetailsUrl } from "../../navigation";
import { Link } from "react-router-dom";
import { getDetailsUrl } from "../kube-object";
interface Props {
subset: EndpointSubset;
@@ -3,10 +3,10 @@ import { observer } from "mobx-react";
import React from "react";
import { Table, TableHead, TableCell, TableRow } from "../table";
import { prevDefault } from "../../utils";
import { showDetails } from "../../navigation";
import { Trans } from "@lingui/macro";
import { endpointStore } from "../+network-endpoints/endpoints.store";
import { Spinner } from "../spinner";
import { showDetails } from "../kube-object";
interface Props {
endpoint: KubeObject;
+2 -2
View File
@@ -8,13 +8,13 @@ import { Services, servicesRoute, servicesURL } from "../+network-services";
import { endpointRoute, Endpoints, endpointURL } from "../+network-endpoints";
import { Ingresses, ingressRoute, ingressURL } from "../+network-ingresses";
import { NetworkPolicies, networkPoliciesRoute, networkPoliciesURL } from "../+network-policies";
import { namespaceStore } from "../+namespaces/namespace.store";
import { namespaceUrlParam } from "../+namespaces/namespace.store";
import { isAllowedResource } from "../../../common/rbac";
@observer
export class Network extends React.Component {
static get tabRoutes(): TabLayoutRoute[] {
const query = namespaceStore.getContextParams();
const query = namespaceUrlParam.toObjectParam();
const routes: TabLayoutRoute[] = [];
if (isAllowedResource("services")) {
@@ -10,13 +10,11 @@ import { podsStore } from "../+workloads-pods/pods.store";
import { Link } from "react-router-dom";
import { KubeEventDetails } from "../+events/kube-event-details";
import { volumeClaimStore } from "./volume-claim.store";
import { getDetailsUrl } from "../../navigation";
import { ResourceMetrics } from "../resource-metrics";
import { VolumeClaimDiskChart } from "./volume-claim-disk-chart";
import { KubeObjectDetailsProps } from "../kube-object";
import { getDetailsUrl, KubeObjectDetailsProps, KubeObjectMeta } from "../kube-object";
import { PersistentVolumeClaim } from "../../api/endpoints";
import { _i18n } from "../../i18n";
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
interface Props extends KubeObjectDetailsProps<PersistentVolumeClaim> {
@@ -7,11 +7,10 @@ import { Trans } from "@lingui/macro";
import { volumeClaimStore } from "./volume-claim.store";
import { PersistentVolumeClaim } from "../../api/endpoints/persistent-volume-claims.api";
import { podsStore } from "../+workloads-pods/pods.store";
import { KubeObjectListLayout } from "../kube-object";
import { getDetailsUrl, KubeObjectListLayout } from "../kube-object";
import { IVolumeClaimsRouteParams } from "./volume-claims.route";
import { unitsToBytes } from "../../utils/convertMemory";
import { stopPropagation } from "../../utils";
import { getDetailsUrl } from "../../navigation";
import { storageClassApi } from "../../api/endpoints";
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
@@ -8,9 +8,8 @@ import { observer } from "mobx-react";
import { DrawerItem, DrawerTitle } from "../drawer";
import { Badge } from "../badge";
import { KubeEventDetails } from "../+events/kube-event-details";
import { getDetailsUrl } from "../../navigation";
import { PersistentVolume, pvcApi } from "../../api/endpoints";
import { KubeObjectDetailsProps } from "../kube-object";
import { getDetailsUrl, KubeObjectDetailsProps } from "../kube-object";
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
@@ -5,10 +5,9 @@ import { observer } from "mobx-react";
import { Trans } from "@lingui/macro";
import { Link, RouteComponentProps } from "react-router-dom";
import { PersistentVolume } from "../../api/endpoints/persistent-volume.api";
import { KubeObjectListLayout } from "../kube-object";
import { getDetailsUrl, KubeObjectListLayout } from "../kube-object";
import { IVolumesRouteParams } from "./volumes.route";
import { stopPropagation } from "../../utils";
import { getDetailsUrl } from "../../navigation";
import { volumesStore } from "./volumes.store";
import { pvcApi, storageClassApi } from "../../api/endpoints";
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
+2 -2
View File
@@ -7,14 +7,14 @@ import { TabLayout, TabLayoutRoute } from "../layout/tab-layout";
import { PersistentVolumes, volumesRoute, volumesURL } from "../+storage-volumes";
import { StorageClasses, storageClassesRoute, storageClassesURL } from "../+storage-classes";
import { PersistentVolumeClaims, volumeClaimsRoute, volumeClaimsURL } from "../+storage-volume-claims";
import { namespaceStore } from "../+namespaces/namespace.store";
import { namespaceUrlParam } from "../+namespaces/namespace.store";
import { isAllowedResource } from "../../../common/rbac";
@observer
export class Storage extends React.Component {
static get tabRoutes() {
const tabRoutes: TabLayoutRoute[] = [];
const query = namespaceStore.getContextParams();
const query = namespaceUrlParam.toObjectParam();
tabRoutes.push({
title: <Trans>Persistent Volume Claims</Trans>,
@@ -16,11 +16,11 @@ import { NamespaceSelect } from "../+namespaces/namespace-select";
import { Checkbox } from "../checkbox";
import { KubeObject } from "../../api/kube-object";
import { Notifications } from "../notifications";
import { showDetails } from "../../navigation";
import { rolesStore } from "../+user-management-roles/roles.store";
import { namespaceStore } from "../+namespaces/namespace.store";
import { serviceAccountsStore } from "../+user-management-service-accounts/service-accounts.store";
import { roleBindingsStore } from "./role-bindings.store";
import { showDetails } from "../kube-object";
interface BindingSelectOption extends SelectOption {
value: string; // binding name
@@ -10,7 +10,7 @@ import { Wizard, WizardStep } from "../wizard";
import { Notifications } from "../notifications";
import { rolesStore } from "./roles.store";
import { Input } from "../input";
import { showDetails } from "../../navigation";
import { showDetails } from "../kube-object";
interface Props extends Partial<DialogProps> {
}
@@ -13,7 +13,7 @@ import { Input } from "../input";
import { systemName } from "../input/input_validators";
import { NamespaceSelect } from "../+namespaces/namespace-select";
import { Notifications } from "../notifications";
import { showDetails } from "../../navigation";
import { showDetails } from "../kube-object";
interface Props extends Partial<DialogProps> {
}
@@ -11,8 +11,7 @@ import { secretsStore } from "../+config-secrets/secrets.store";
import { Link } from "react-router-dom";
import { Secret, ServiceAccount } from "../../api/endpoints";
import { KubeEventDetails } from "../+events/kube-event-details";
import { getDetailsUrl } from "../../navigation";
import { KubeObjectDetailsProps } from "../kube-object";
import { getDetailsUrl, KubeObjectDetailsProps } from "../kube-object";
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { Icon } from "../icon";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
@@ -7,7 +7,7 @@ import { Roles } from "../+user-management-roles";
import { RoleBindings } from "../+user-management-roles-bindings";
import { ServiceAccounts } from "../+user-management-service-accounts";
import { roleBindingsRoute, roleBindingsURL, rolesRoute, rolesURL, serviceAccountsRoute, serviceAccountsURL } from "./user-management.route";
import { namespaceStore } from "../+namespaces/namespace.store";
import { namespaceUrlParam } from "../+namespaces/namespace.store";
import { PodSecurityPolicies, podSecurityPoliciesRoute, podSecurityPoliciesURL } from "../+pod-security-policies";
import { isAllowedResource } from "../../../common/rbac";
@@ -15,7 +15,7 @@ import { isAllowedResource } from "../../../common/rbac";
export class UserManagement extends React.Component {
static get tabRoutes() {
const tabRoutes: TabLayoutRoute[] = [];
const query = namespaceStore.getContextParams();
const query = namespaceUrlParam.toObjectParam();
tabRoutes.push(
{
@@ -10,8 +10,7 @@ import { jobStore } from "../+workloads-jobs/job.store";
import { Link } from "react-router-dom";
import { KubeEventDetails } from "../+events/kube-event-details";
import { cronJobStore } from "./cronjob.store";
import { getDetailsUrl } from "../../navigation";
import { KubeObjectDetailsProps } from "../kube-object";
import { getDetailsUrl, KubeObjectDetailsProps } from "../kube-object";
import { CronJob, Job } from "../../api/endpoints";
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
@@ -13,8 +13,7 @@ import { PodDetailsAffinities } from "../+workloads-pods/pod-details-affinities"
import { KubeEventDetails } from "../+events/kube-event-details";
import { podsStore } from "../+workloads-pods/pods.store";
import { jobStore } from "./job.store";
import { getDetailsUrl } from "../../navigation";
import { KubeObjectDetailsProps } from "../kube-object";
import { getDetailsUrl, KubeObjectDetailsProps } from "../kube-object";
import { Job } from "../../api/endpoints";
import { PodDetailsList } from "../+workloads-pods/pod-details-list";
import { lookupApiLink } from "../../api/kube-api";
@@ -2,6 +2,7 @@ import "./pod-details-list.scss";
import React from "react";
import kebabCase from "lodash/kebabCase";
import { reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import { Trans } from "@lingui/macro";
import { podsStore } from "./pods.store";
@@ -10,11 +11,10 @@ import { autobind, bytesToUnits, cssNames, interval, prevDefault } from "../../u
import { LineProgress } from "../line-progress";
import { KubeObject } from "../../api/kube-object";
import { Table, TableCell, TableHead, TableRow } from "../table";
import { showDetails } from "../../navigation";
import { reaction } from "mobx";
import { Spinner } from "../spinner";
import { DrawerTitle } from "../drawer";
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import { showDetails } from "../kube-object";
enum sortBy {
name = "name",
@@ -5,7 +5,7 @@ import { Link } from "react-router-dom";
import { autorun, observable } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import { Pod, Secret, secretsApi } from "../../api/endpoints";
import { getDetailsUrl } from "../../navigation";
import { getDetailsUrl } from "../kube-object";
interface Props {
pod: Pod;
@@ -18,8 +18,7 @@ import { KubeEventDetails } from "../+events/kube-event-details";
import { PodDetailsSecrets } from "./pod-details-secrets";
import { ResourceMetrics } from "../resource-metrics";
import { podsStore } from "./pods.store";
import { getDetailsUrl } from "../../navigation";
import { KubeObjectDetailsProps } from "../kube-object";
import { getDetailsUrl, KubeObjectDetailsProps } from "../kube-object";
import { getItemMetrics } from "../../api/endpoints/metrics.api";
import { PodCharts, podMetricTabs } from "./pod-charts";
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
@@ -9,11 +9,10 @@ import { RouteComponentProps } from "react-router";
import { volumeClaimStore } from "../+storage-volume-claims/volume-claim.store";
import { IPodsRouteParams } from "../+workloads";
import { eventStore } from "../+events/event.store";
import { KubeObjectListLayout } from "../kube-object";
import { getDetailsUrl, KubeObjectListLayout } from "../kube-object";
import { nodesApi, Pod } from "../../api/endpoints";
import { StatusBrick } from "../status-brick";
import { cssNames, stopPropagation } from "../../utils";
import { getDetailsUrl } from "../../navigation";
import toPairs from "lodash/toPairs";
import startCase from "lodash/startCase";
import kebabCase from "lodash/kebabCase";
@@ -6,7 +6,7 @@ import { Trans } from "@lingui/macro";
import { TabLayout, TabLayoutRoute } from "../layout/tab-layout";
import { WorkloadsOverview } from "../+workloads-overview/overview";
import { cronJobsRoute, cronJobsURL, daemonSetsRoute, daemonSetsURL, deploymentsRoute, deploymentsURL, jobsRoute, jobsURL, overviewRoute, overviewURL, podsRoute, podsURL, replicaSetsRoute, replicaSetsURL, statefulSetsRoute, statefulSetsURL } from "./workloads.route";
import { namespaceStore } from "../+namespaces/namespace.store";
import { namespaceUrlParam } from "../+namespaces/namespace.store";
import { Pods } from "../+workloads-pods";
import { Deployments } from "../+workloads-deployments";
import { DaemonSets } from "../+workloads-daemonsets";
@@ -19,7 +19,7 @@ import { ReplicaSets } from "../+workloads-replicasets";
@observer
export class Workloads extends React.Component {
static get tabRoutes(): TabLayoutRoute[] {
const query = namespaceStore.getContextParams();
const query = namespaceUrlParam.toObjectParam();
const routes: TabLayoutRoute[] = [
{
title: <Trans>Overview</Trans>,
+12 -13
View File
@@ -41,10 +41,10 @@ import { broadcastMessage, requestMain } from "../../common/ipc";
import whatInput from "what-input";
import { clusterSetFrameIdHandler } from "../../common/cluster-ipc";
import { ClusterPageMenuRegistration, clusterPageMenuRegistry } from "../../extensions/registries";
import { TabLayoutRoute, TabLayout } from "./layout/tab-layout";
import { TabLayout, TabLayoutRoute } from "./layout/tab-layout";
import { StatefulSetScaleDialog } from "./+workloads-statefulsets/statefulset-scale-dialog";
import { eventStore } from "./+events/event.store";
import { reaction, computed } from "mobx";
import { computed, reaction } from "mobx";
import { nodesStore } from "./+nodes/nodes.store";
import { podsStore } from "./+workloads-pods/pods.store";
import { sum } from "lodash";
@@ -129,16 +129,15 @@ export class App extends React.Component {
if (!menuItem.id) {
return routes;
}
clusterPageMenuRegistry.getSubItems(menuItem).forEach((item) => {
const page = clusterPageRegistry.getByPageMenuTarget(item.target);
clusterPageMenuRegistry.getSubItems(menuItem).forEach((subMenu) => {
const page = clusterPageRegistry.getByPageTarget(subMenu.target);
if (page) {
routes.push({
routePath: page.routePath,
url: getExtensionPageUrl({ extensionId: page.extensionId, pageId: page.id, params: item.target.params }),
title: item.title,
routePath: page.url,
url: getExtensionPageUrl(subMenu.target),
title: subMenu.title,
component: page.components.Page,
exact: page.exact
});
}
});
@@ -151,14 +150,14 @@ export class App extends React.Component {
const tabRoutes = this.getTabLayoutRoutes(menu);
if (tabRoutes.length > 0) {
const pageComponent = () => <TabLayout tabs={tabRoutes} />;
const pageComponent = () => <TabLayout tabs={tabRoutes}/>;
return <Route key={`extension-tab-layout-route-${index}`} component={pageComponent} path={tabRoutes.map((tab) => tab.routePath)} />;
return <Route key={`extension-tab-layout-route-${index}`} component={pageComponent} path={tabRoutes.map((tab) => tab.routePath)}/>;
} else {
const page = clusterPageRegistry.getByPageMenuTarget(menu.target);
const page = clusterPageRegistry.getByPageTarget(menu.target);
if (page) {
return <Route key={`extension-tab-layout-route-${index}`} path={page.routePath} exact={page.exact} component={page.components.Page}/>;
return <Route key={`extension-tab-layout-route-${index}`} path={page.url} component={page.components.Page}/>;
}
}
});
@@ -169,7 +168,7 @@ export class App extends React.Component {
const menu = clusterPageMenuRegistry.getByPage(page);
if (!menu) {
return <Route key={`extension-route-${index}`} path={page.routePath} exact={page.exact} component={page.components.Page}/>;
return <Route key={`extension-route-${index}`} path={page.url} component={page.components.Page}/>;
}
});
}
@@ -71,8 +71,8 @@ export class ClusterManager extends React.Component {
<Route component={AddCluster} {...addClusterRoute} />
<Route component={ClusterView} {...clusterViewRoute} />
<Route component={ClusterSettings} {...clusterSettingsRoute} />
{globalPageRegistry.getItems().map(({ routePath, exact, components: { Page } }) => {
return <Route key={routePath} path={routePath} component={Page} exact={exact}/>;
{globalPageRegistry.getItems().map(({ url, components: { Page } }) => {
return <Route key={url} path={url} component={Page}/>;
})}
<Redirect exact to={this.startUrl}/>
</Switch>
@@ -15,7 +15,7 @@ import { ClusterIcon } from "../cluster-icon";
import { Icon } from "../icon";
import { autobind, cssNames, IClassName } from "../../utils";
import { Badge } from "../badge";
import { navigate, navigation } from "../../navigation";
import { isActiveRoute, navigate } from "../../navigation";
import { addClusterURL } from "../+add-cluster";
import { clusterSettingsURL } from "../+cluster-settings";
import { landingURL } from "../+landing-page";
@@ -158,12 +158,13 @@ export class ClustersMenu extends React.Component<Props> {
</div>
<div className="extensions">
{globalPageMenuRegistry.getItems().map(({ title, target, components: { Icon } }) => {
const registeredPage = globalPageRegistry.getByPageMenuTarget(target);
const registeredPage = globalPageRegistry.getByPageTarget(target);
if (!registeredPage) return;
const { extensionId, id: pageId } = registeredPage;
const pageUrl = getExtensionPageUrl({ extensionId, pageId, params: target.params });
const isActive = pageUrl === navigation.location.pathname;
if (!registeredPage){
return;
}
const pageUrl = getExtensionPageUrl(target);
const isActive = isActiveRoute(registeredPage.url);
return (
<Icon
+7 -1
View File
@@ -114,7 +114,13 @@ export class Icon extends React.PureComponent<IconProps> {
// render icon type
if (link) {
return <NavLink {...iconProps} to={link}/>;
const { className, children } = iconProps;
return (
<NavLink className={className} to={link}>
{children}
</NavLink>
);
}
if (href) {
@@ -2,9 +2,15 @@ import React from "react";
import debounce from "lodash/debounce";
import { autorun, observable } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import { getSearch, setSearch } from "../../navigation";
import { InputProps } from "./input";
import { SearchInput } from "./search-input";
import { createPageParam } from "../../navigation";
export const searchUrlParam = createPageParam({
name: "search",
isSystem: true,
defaultValue: "",
});
interface Props extends InputProps {
compact?: boolean; // show only search-icon when not focused
@@ -12,11 +18,11 @@ interface Props extends InputProps {
@observer
export class SearchInputUrl extends React.Component<Props> {
@observable inputVal = ""; // fix: use empty string to avoid react warnings
@observable inputVal = ""; // fix: use empty string on init to avoid react warnings
@disposeOnUnmount
updateInput = autorun(() => this.inputVal = getSearch());
updateUrl = debounce((val: string) => setSearch(val), 250);
updateInput = autorun(() => this.inputVal = searchUrlParam.get());
updateUrl = debounce((val: string) => searchUrlParam.set(val), 250);
setValue = (value: string) => {
this.inputVal = value;
@@ -1,7 +1,7 @@
import { computed, observable, reaction } from "mobx";
import { autobind } from "../../utils";
import { getSearch, setSearch } from "../../navigation";
import { namespaceStore } from "../+namespaces/namespace.store";
import { searchUrlParam } from "../input/search-input-url";
export enum FilterType {
SEARCH = "search",
@@ -54,8 +54,8 @@ export class PageFiltersStore {
protected syncWithGlobalSearch() {
const disposers = [
reaction(() => this.getValues(FilterType.SEARCH)[0], setSearch),
reaction(() => getSearch(), search => {
reaction(() => this.getValues(FilterType.SEARCH)[0], search => searchUrlParam.set(search)),
reaction(() => searchUrlParam.get(), search => {
const filter = this.getByType(FilterType.SEARCH);
if (filter) {
@@ -4,7 +4,7 @@ import React from "react";
import { disposeOnUnmount, observer } from "mobx-react";
import { computed, observable, reaction } from "mobx";
import { Trans } from "@lingui/macro";
import { getDetails, hideDetails } from "../../navigation";
import { createPageParam, navigation } from "../../navigation";
import { Drawer } from "../drawer";
import { KubeObject } from "../../api/kube-object";
import { Spinner } from "../spinner";
@@ -14,6 +14,43 @@ import { CrdResourceDetails } from "../+custom-resources";
import { KubeObjectMenu } from "./kube-object-menu";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
export const kubeDetailsUrlParam = createPageParam({
name: "kube-details",
isSystem: true,
});
export const kubeSelectedUrlParam = createPageParam({
name: "kube-selected",
isSystem: true,
get defaultValue() {
return kubeDetailsUrlParam.get();
}
});
export function showDetails(details = "", resetSelected = true) {
const detailsUrl = getDetailsUrl(details, resetSelected);
navigation.merge({ search: detailsUrl });
}
export function hideDetails() {
showDetails();
}
export function getDetailsUrl(details: string, resetSelected = false) {
const detailsUrl = kubeDetailsUrlParam.toSearchString({ value: details });
if (resetSelected) {
const params = new URLSearchParams(detailsUrl);
params.delete(kubeSelectedUrlParam.name);
return `?${params.toString()}`;
}
return detailsUrl;
}
export interface KubeObjectDetailsProps<T = KubeObject> {
className?: string;
object: T;
@@ -25,7 +62,7 @@ export class KubeObjectDetails extends React.Component {
@observable.ref loadingError: React.ReactNode;
@computed get path() {
return getDetails();
return kubeDetailsUrlParam.get();
}
@computed get object() {
@@ -70,7 +107,7 @@ export class KubeObjectDetails extends React.Component {
const { object, isLoading, loadingError, isCrdInstance } = this;
const isOpen = !!(object || isLoading || loadingError);
let title = "";
let details: JSX.Element[];
let details: React.ReactNode[];
if (object) {
const { kind, getName } = object;
@@ -81,7 +118,7 @@ export class KubeObjectDetails extends React.Component {
});
if (isCrdInstance && details.length === 0) {
details.push(<CrdResourceDetails object={object} />);
details.push(<CrdResourceDetails object={object}/>);
}
}
@@ -90,7 +127,7 @@ export class KubeObjectDetails extends React.Component {
className="KubeObjectDetails flex column"
open={isOpen}
title={title}
toolbar={<KubeObjectMenu object={object} toolbar={true} />}
toolbar={<KubeObjectMenu object={object} toolbar={true}/>}
onClose={hideDetails}
>
{isLoading && <Spinner center/>}
@@ -3,10 +3,10 @@ import { computed } from "mobx";
import { observer } from "mobx-react";
import { cssNames } from "../../utils";
import { KubeObject } from "../../api/kube-object";
import { getSelectedDetails, showDetails } from "../../navigation";
import { ItemListLayout, ItemListLayoutProps } from "../item-object-list/item-list-layout";
import { KubeObjectStore } from "../../kube-object.store";
import { KubeObjectMenu } from "./kube-object-menu";
import { kubeSelectedUrlParam, showDetails } from "./kube-object-details";
export interface KubeObjectListLayoutProps extends ItemListLayoutProps {
store: KubeObjectStore;
@@ -15,14 +15,13 @@ export interface KubeObjectListLayoutProps extends ItemListLayoutProps {
@observer
export class KubeObjectListLayout extends React.Component<KubeObjectListLayoutProps> {
@computed get selectedItem() {
return this.props.store.getByPath(getSelectedDetails());
return this.props.store.getByPath(kubeSelectedUrlParam.get());
}
onDetails = (item: KubeObject) => {
if (this.props.onDetails) {
this.props.onDetails(item);
}
else {
} else {
showDetails(item.selfLink);
}
};
@@ -4,7 +4,7 @@ import { autobind, cssNames } from "../../utils";
import { KubeObject } from "../../api/kube-object";
import { editResourceTab } from "../dock/edit-resource.store";
import { MenuActions, MenuActionsProps } from "../menu/menu-actions";
import { hideDetails } from "../../navigation";
import { hideDetails } from "./kube-object-details";
import { apiManager } from "../../api/api-manager";
import { kubeObjectMenuRegistry } from "../../../extensions/registries/kube-object-menu-registry";
@@ -2,10 +2,10 @@ import React from "react";
import { Trans } from "@lingui/macro";
import { IKubeMetaField, KubeObject } from "../../api/kube-object";
import { DrawerItem, DrawerItemLabels } from "../drawer";
import { getDetailsUrl } from "../../navigation";
import { lookupApiLink } from "../../api/kube-api";
import { Link } from "react-router-dom";
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import { getDetailsUrl } from "./kube-object-details";
export interface KubeObjectMetaProps {
object: KubeObject;
+15 -13
View File
@@ -17,7 +17,7 @@ import { clusterRoute, clusterURL } from "../+cluster";
import { Config, configRoute, configURL } from "../+config";
import { eventRoute, eventsURL } from "../+events";
import { Apps, appsRoute, appsURL } from "../+apps";
import { namespaceStore } from "../+namespaces/namespace.store";
import { namespaceUrlParam } from "../+namespaces/namespace.store";
import { Workloads } from "../+workloads";
import { UserManagement } from "../+user-management";
import { Storage } from "../+storage";
@@ -75,21 +75,23 @@ export class Sidebar extends React.Component<Props> {
}
getTabLayoutRoutes(menu: ClusterPageMenuRegistration): TabLayoutRoute[] {
if (!menu.id) {
return [];
}
const routes: TabLayoutRoute[] = [];
clusterPageMenuRegistry.getSubItems(menu).forEach((subItem) => {
const subPage = clusterPageRegistry.getByPageMenuTarget(subItem.target);
if (!menu.id) {
return routes;
}
clusterPageMenuRegistry.getSubItems(menu).forEach((subMenu) => {
const subPage = clusterPageRegistry.getByPageTarget(subMenu.target);
if (subPage) {
const { extensionId, id: pageId } = subPage;
routes.push({
routePath: subPage.routePath,
url: getExtensionPageUrl({ extensionId: subPage.extensionId, pageId: subPage.id, params: subItem.target.params }),
title: subItem.title,
routePath: subPage.url,
url: getExtensionPageUrl({ extensionId, pageId, params: subMenu.target.params }),
title: subMenu.title,
component: subPage.components.Page,
exact: subPage.exact
});
}
});
@@ -99,7 +101,7 @@ export class Sidebar extends React.Component<Props> {
renderRegisteredMenus() {
return clusterPageMenuRegistry.getRootItems().map((menuItem, index) => {
const registeredPage = clusterPageRegistry.getByPageMenuTarget(menuItem.target);
const registeredPage = clusterPageRegistry.getByPageTarget(menuItem.target);
const tabRoutes = this.getTabLayoutRoutes(menuItem);
const id = `registered-item-${index}`;
let pageUrl: string;
@@ -109,7 +111,7 @@ export class Sidebar extends React.Component<Props> {
const { extensionId, id: pageId } = registeredPage;
pageUrl = getExtensionPageUrl({ extensionId, pageId, params: menuItem.target.params });
isActive = isActiveRoute(registeredPage.routePath);
isActive = isActiveRoute(registeredPage.url);
} else if (tabRoutes.length > 0) {
pageUrl = tabRoutes[0].url;
isActive = isActiveRoute(tabRoutes.map((tab) => tab.routePath));
@@ -133,7 +135,7 @@ export class Sidebar extends React.Component<Props> {
render() {
const { toggle, isPinned, className } = this.props;
const query = namespaceStore.getContextParams();
const query = namespaceUrlParam.toObjectParam();
return (
<SidebarContext.Provider value={{ pinned: isPinned }}>
+28 -30
View File
@@ -1,19 +1,17 @@
import "./table.scss";
import React from "react";
import { orderBy } from "lodash";
import { observer } from "mobx-react";
import { computed, observable } from "mobx";
import { observable } from "mobx";
import { autobind, cssNames, noop } from "../../utils";
import { TableRow, TableRowElem, TableRowProps } from "./table-row";
import { TableHead, TableHeadElem, TableHeadProps } from "./table-head";
import { TableCellElem } from "./table-cell";
import { VirtualList } from "../virtual-list";
import { navigation, setQueryParams } from "../../navigation";
import orderBy from "lodash/orderBy";
import { createPageParam } from "../../navigation";
import { ItemObject } from "../../item.store";
// todo: refactor + decouple search from location
export type TableSortBy = string;
export type TableOrderBy = "asc" | "desc" | string;
export type TableSortParams = { sortBy: TableSortBy; orderBy: TableOrderBy };
@@ -43,6 +41,16 @@ export interface TableProps extends React.DOMAttributes<HTMLDivElement> {
getTableRow?: (uid: string) => React.ReactElement<TableRowProps>;
}
export const sortByUrlParam = createPageParam({
name: "sort",
isSystem: true,
});
export const orderByUrlParam = createPageParam({
name: "order",
isSystem: true,
});
@observer
export class Table extends React.Component<TableProps> {
static defaultProps: TableProps = {
@@ -53,18 +61,13 @@ export class Table extends React.Component<TableProps> {
sortSyncWithUrl: true,
};
@observable sortParamsLocal = this.props.sortByDefault;
@computed get sortParams(): Partial<TableSortParams> {
if (this.props.sortSyncWithUrl) {
const sortBy = navigation.searchParams.get("sortBy");
const orderBy = navigation.searchParams.get("orderBy");
return { sortBy, orderBy };
}
return this.sortParamsLocal || {};
}
@observable sortParams: Partial<TableSortParams> = Object.assign(
this.props.sortSyncWithUrl ? {
sortBy: sortByUrlParam.get(),
orderBy: orderByUrlParam.get(),
} : {},
this.props.sortByDefault,
);
renderHead() {
const { sortable, children } = this.props;
@@ -101,29 +104,24 @@ export class Table extends React.Component<TableProps> {
}
getSorted(items: any[]) {
const { sortParams } = this;
const sortingCallback = this.props.sortable[sortParams.sortBy] || noop;
const { sortBy, orderBy: order } = this.sortParams;
const sortingCallback = this.props.sortable[sortBy] || noop;
return orderBy(
items,
sortingCallback,
sortParams.orderBy as any
);
return orderBy(items, sortingCallback, order as any);
}
@autobind()
protected onSort(params: TableSortParams) {
protected onSort({ sortBy, orderBy }: TableSortParams) {
this.sortParams = { sortBy, orderBy };
const { sortSyncWithUrl, onSort } = this.props;
if (sortSyncWithUrl) {
setQueryParams(params);
}
else {
this.sortParamsLocal = params;
sortByUrlParam.set(sortBy);
orderByUrlParam.set(orderBy);
}
if (onSort) {
onSort(params);
onSort({ sortBy, orderBy });
}
}