1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Fix releases not reloading when selecting namespaces (#2515)

This commit is contained in:
Sebastian Malton 2021-04-15 10:55:55 -04:00 committed by GitHub
parent 4e231e5749
commit 10bcae24e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 140 additions and 119 deletions

View File

@ -86,7 +86,7 @@ export async function clickWhatsNew(app: Application) {
export async function clickWelcomeNotification(app: Application) { export async function clickWelcomeNotification(app: Application) {
const itemsText = await app.client.$("div.info-panel").getText(); const itemsText = await app.client.$("div.info-panel").getText();
if (itemsText === "0 item") { if (itemsText === "0 items") {
// welcome notification should be present, dismiss it // welcome notification should be present, dismiss it
await app.client.waitUntilTextExists("div.message", "Welcome!"); await app.client.waitUntilTextExists("div.message", "Welcome!");
await app.client.click("i.Icon.close"); await app.client.click("i.Icon.close");

View File

@ -14,8 +14,18 @@ export interface IChartVersion {
export class HelmChartStore extends ItemStore<HelmChart> { export class HelmChartStore extends ItemStore<HelmChart> {
@observable versions = observable.map<string, IChartVersion[]>(); @observable versions = observable.map<string, IChartVersion[]>();
loadAll() { async loadAll() {
return this.loadItems(() => helmChartsApi.list()); try {
const res = await this.loadItems(() => helmChartsApi.list());
this.failedLoading = false;
return res;
} catch (error) {
this.failedLoading = true;
throw error;
}
} }
getByName(name: string, repo: string) { getByName(name: string, repo: string) {

View File

@ -1,5 +1,5 @@
import isEqual from "lodash/isEqual"; import isEqual from "lodash/isEqual";
import { action, IReactionDisposer, observable, reaction, when } from "mobx"; import { action, observable, reaction, when } from "mobx";
import { autobind } from "../../utils"; import { autobind } from "../../utils";
import { HelmRelease, helmReleasesApi, IReleaseCreatePayload, IReleaseUpdatePayload } from "../../api/endpoints/helm-releases.api"; import { HelmRelease, helmReleasesApi, IReleaseCreatePayload, IReleaseUpdatePayload } from "../../api/endpoints/helm-releases.api";
import { ItemStore } from "../../item.store"; import { ItemStore } from "../../item.store";
@ -10,64 +10,64 @@ import { Notifications } from "../notifications";
@autobind() @autobind()
export class ReleaseStore extends ItemStore<HelmRelease> { export class ReleaseStore extends ItemStore<HelmRelease> {
@observable releaseSecrets: Secret[] = []; releaseSecrets = observable.map<string, Secret>();
@observable secretWatcher: IReactionDisposer;
constructor() { constructor() {
super(); super();
when(() => secretsStore.isLoaded, () => { when(() => secretsStore.isLoaded, () => {
this.releaseSecrets = this.getReleaseSecrets(); this.releaseSecrets.replace(this.getReleaseSecrets());
}); });
} }
watch() { watchAssociatedSecrets(): (() => void) {
this.secretWatcher = reaction(() => secretsStore.items.toJS(), () => { return reaction(() => secretsStore.items.toJS(), () => {
if (this.isLoading) return; if (this.isLoading) return;
const secrets = this.getReleaseSecrets(); const newSecrets = this.getReleaseSecrets();
const amountChanged = secrets.length !== this.releaseSecrets.length; const amountChanged = newSecrets.length !== this.releaseSecrets.size;
const labelsChanged = this.releaseSecrets.some(item => { const labelsChanged = newSecrets.some(([id, secret]) => (
const secret = secrets.find(secret => secret.getId() == item.getId()); !isEqual(secret.getLabels(), this.releaseSecrets.get(id)?.getLabels())
));
if (!secret) return;
return !isEqual(item.getLabels(), secret.getLabels());
});
if (amountChanged || labelsChanged) { if (amountChanged || labelsChanged) {
this.loadFromContextNamespaces(); this.loadFromContextNamespaces();
} }
this.releaseSecrets = [...secrets]; this.releaseSecrets.replace(newSecrets);
}); });
} }
unwatch() { watchSelecteNamespaces(): (() => void) {
this.secretWatcher(); return reaction(() => namespaceStore.context.contextNamespaces, namespaces => {
this.loadAll(namespaces);
});
} }
getReleaseSecrets() { private getReleaseSecrets() {
return secretsStore.getByLabel({ owner: "helm" }); return secretsStore
.getByLabel({ owner: "helm" })
.map(s => [s.getId(), s] as const);
} }
getReleaseSecret(release: HelmRelease) { getReleaseSecret(release: HelmRelease) {
const labels = { return secretsStore.getByLabel({
owner: "helm", owner: "helm",
name: release.getName() name: release.getName()
}; })
.find(secret => secret.getNs() == release.getNs());
return secretsStore.getByLabel(labels)
.filter(secret => secret.getNs() == release.getNs())[0];
} }
@action @action
async loadAll(namespaces: string[]) { async loadAll(namespaces: string[]) {
this.isLoading = true; this.isLoading = true;
this.isLoaded = false;
try { try {
const items = await this.loadItems(namespaces); const items = await this.loadItems(namespaces);
this.items.replace(this.sortItems(items)); this.items.replace(this.sortItems(items));
this.isLoaded = true; this.isLoaded = true;
this.failedLoading = false;
} catch (error) { } catch (error) {
this.failedLoading = true;
console.error("Loading Helm Chart releases has failed", error); console.error("Loading Helm Chart releases has failed", error);
if (error.error) { if (error.error) {
@ -79,17 +79,18 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
} }
async loadFromContextNamespaces(): Promise<void> { async loadFromContextNamespaces(): Promise<void> {
return this.loadAll(namespaceStore.contextNamespaces); return this.loadAll(namespaceStore.context.contextNamespaces);
} }
async loadItems(namespaces: string[]) { async loadItems(namespaces: string[]) {
const isLoadingAll = namespaceStore.allowedNamespaces.every(ns => namespaces.includes(ns)); const isLoadingAll = namespaceStore.context.allNamespaces?.length > 1
const noAccessibleNamespaces = namespaceStore.context.cluster.accessibleNamespaces.length === 0; && namespaceStore.context.cluster.accessibleNamespaces.length === 0
&& namespaceStore.context.allNamespaces.every(ns => namespaces.includes(ns));
if (isLoadingAll && noAccessibleNamespaces) { if (isLoadingAll) {
return helmReleasesApi.list(); return helmReleasesApi.list();
} else { } else {
return Promise return Promise // load resources per namespace
.all(namespaces.map(namespace => helmReleasesApi.list(namespace))) .all(namespaces.map(namespace => helmReleasesApi.list(namespace)))
.then(items => items.flat()); .then(items => items.flat());
} }

View File

@ -2,7 +2,7 @@ import "./releases.scss";
import React, { Component } from "react"; import React, { Component } from "react";
import kebabCase from "lodash/kebabCase"; import kebabCase from "lodash/kebabCase";
import { observer } from "mobx-react"; import { disposeOnUnmount, observer } from "mobx-react";
import { RouteComponentProps } from "react-router"; import { RouteComponentProps } from "react-router";
import { releaseStore } from "./release.store"; import { releaseStore } from "./release.store";
import { IReleaseRouteParams, releaseURL } from "./release.route"; import { IReleaseRouteParams, releaseURL } from "./release.route";
@ -30,14 +30,11 @@ interface Props extends RouteComponentProps<IReleaseRouteParams> {
@observer @observer
export class HelmReleases extends Component<Props> { export class HelmReleases extends Component<Props> {
componentDidMount() { componentDidMount() {
// Watch for secrets associated with releases and react to their changes disposeOnUnmount(this, [
releaseStore.watch(); releaseStore.watchAssociatedSecrets(),
} releaseStore.watchSelecteNamespaces(),
]);
componentWillUnmount() {
releaseStore.unwatch();
} }
get selectedRelease() { get selectedRelease() {
@ -49,21 +46,16 @@ export class HelmReleases extends Component<Props> {
} }
showDetails = (item: HelmRelease) => { showDetails = (item: HelmRelease) => {
if (!item) { navigation.merge(releaseURL({
navigation.merge(releaseURL()); params: {
} name: item.getName(),
else { namespace: item.getNs()
navigation.merge(releaseURL({ }
params: { }));
name: item.getName(),
namespace: item.getNs()
}
}));
}
}; };
hideDetails = () => { hideDetails = () => {
this.showDetails(null); navigation.merge(releaseURL());
}; };
renderRemoveDialogMessage(selectedItems: HelmRelease[]) { renderRemoveDialogMessage(selectedItems: HelmRelease[]) {
@ -114,30 +106,22 @@ export class HelmReleases extends Component<Props> {
{ title: "Status", className: "status", sortBy: columnId.status, id: columnId.status }, { title: "Status", className: "status", sortBy: columnId.status, id: columnId.status },
{ title: "Updated", className: "updated", sortBy: columnId.updated, id: columnId.updated }, { title: "Updated", className: "updated", sortBy: columnId.updated, id: columnId.updated },
]} ]}
renderTableContents={(release: HelmRelease) => { renderTableContents={(release: HelmRelease) => [
const version = release.getVersion(); release.getName(),
release.getNs(),
return [ release.getChart(),
release.getName(), release.getRevision(),
release.getNs(), release.getVersion(),
release.getChart(), release.appVersion,
release.getRevision(), { title: release.getStatus(), className: kebabCase(release.getStatus()) },
<> release.getUpdated(),
{version} ]}
</>, renderItemMenu={(release: HelmRelease) => (
release.appVersion, <HelmReleaseMenu
{ title: release.getStatus(), className: kebabCase(release.getStatus()) }, release={release}
release.getUpdated(), removeConfirmationMessage={this.renderRemoveDialogMessage([release])}
]; />
}} )}
renderItemMenu={(release: HelmRelease) => {
return (
<HelmReleaseMenu
release={release}
removeConfirmationMessage={this.renderRemoveDialogMessage([release])}
/>
);
}}
customizeRemoveDialog={(selectedItems: HelmRelease[]) => ({ customizeRemoveDialog={(selectedItems: HelmRelease[]) => ({
message: this.renderRemoveDialogMessage(selectedItems) message: this.renderRemoveDialogMessage(selectedItems)
})} })}

View File

@ -65,8 +65,18 @@ export class WorkspaceClusterStore extends ItemStore<ClusterItem> {
}); });
} }
loadAll() { async loadAll() {
return this.loadItems(() => this.clusters); try {
const res = await this.loadItems(() => this.clusters);
this.failedLoading = false;
return res;
} catch (error) {
this.failedLoading = true;
throw error;
}
} }
async remove(clusterItem: ClusterItem) { async remove(clusterItem: ClusterItem) {

View File

@ -4,8 +4,8 @@ import { namespaceStore } from "./+namespaces/namespace.store";
export interface ClusterContext { export interface ClusterContext {
cluster?: Cluster; cluster?: Cluster;
allNamespaces?: string[]; // available / allowed namespaces from cluster.ts allNamespaces: string[]; // available / allowed namespaces from cluster.ts
contextNamespaces?: string[]; // selected by user (see: namespace-select.tsx) contextNamespaces: string[]; // selected by user (see: namespace-select.tsx)
} }
export const clusterContext: ClusterContext = { export const clusterContext: ClusterContext = {

View File

@ -172,6 +172,10 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
return this.props.isReady ?? this.props.store.isLoaded; return this.props.isReady ?? this.props.store.isLoaded;
} }
@computed get failedToLoad() {
return this.props.store.failedLoading;
}
@computed get filters() { @computed get filters() {
let { activeFilters } = pageFilters; let { activeFilters } = pageFilters;
const { isSearchable, searchFilters } = this.props; const { isSearchable, searchFilters } = this.props;
@ -281,6 +285,11 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
}); });
} }
@autobind()
toggleFilters() {
this.showFilters = !this.showFilters;
}
renderFilters() { renderFilters() {
const { hideFilters } = this.props; const { hideFilters } = this.props;
const { isReady, filters } = this; const { isReady, filters } = this;
@ -293,6 +302,14 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
} }
renderNoItems() { renderNoItems() {
if (this.failedToLoad) {
return <NoItems>Failed to load items.</NoItems>;
}
if (!this.isReady) {
return <Spinner center />;
}
if (this.filters.length > 0) { if (this.filters.length > 0) {
return ( return (
<NoItems> <NoItems>
@ -309,6 +326,14 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
return <NoItems/>; return <NoItems/>;
} }
renderItems() {
if (this.props.virtual) {
return null;
}
return this.items.map(item => this.getRow(item.getId()));
}
renderHeaderContent(placeholders: IHeaderPlaceholders): ReactNode { renderHeaderContent(placeholders: IHeaderPlaceholders): ReactNode {
const { isSearchable, searchFilters } = this.props; const { isSearchable, searchFilters } = this.props;
const { title, filters, search, info } = placeholders; const { title, filters, search, info } = placeholders;
@ -317,7 +342,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
<> <>
{title} {title}
<div className="info-panel box grow"> <div className="info-panel box grow">
{this.isReady && info} {info}
</div> </div>
{filters} {filters}
{isSearchable && searchFilters && search} {isSearchable && searchFilters && search}
@ -326,20 +351,17 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
} }
renderInfo() { renderInfo() {
const { items, isReady, filters } = this; const { items, filters } = this;
const allItemsCount = this.props.store.getTotalCount(); const allItemsCount = this.props.store.getTotalCount();
const itemsCount = items.length; const itemsCount = items.length;
const isFiltered = isReady && filters.length > 0;
if (isFiltered) {
const toggleFilters = () => this.showFilters = !this.showFilters;
if (filters.length > 0) {
return ( return (
<><a onClick={toggleFilters}>Filtered</a>: {itemsCount} / {allItemsCount}</> <><a onClick={this.toggleFilters}>Filtered</a>: {itemsCount} / {allItemsCount}</>
); );
} }
return allItemsCount <= 1 ? `${allItemsCount} item` : `${allItemsCount} items`; return allItemsCount === 1 ? `${allItemsCount} item` : `${allItemsCount} items`;
} }
renderHeader() { renderHeader() {
@ -412,40 +434,31 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
renderList() { renderList() {
const { const {
store, hasDetailsView, addRemoveButtons = {}, virtual, sortingCallbacks, detailsItem, store, hasDetailsView, addRemoveButtons = {}, virtual, sortingCallbacks,
tableProps = {}, tableId detailsItem, className, tableProps = {}, tableId,
} = this.props; } = this.props;
const { isReady, removeItemsDialog, items } = this; const { removeItemsDialog, items } = this;
const { selectedItems } = store; const { selectedItems } = store;
const selectedItemId = detailsItem && detailsItem.getId(); const selectedItemId = detailsItem && detailsItem.getId();
const classNames = cssNames(className, "box", "grow", themeStore.activeTheme.type);
return ( return (
<div className="items box grow flex column"> <div className="items box grow flex column">
{!isReady && ( <Table
<Spinner center/> tableId={tableId}
)} virtual={virtual}
{isReady && ( selectable={hasDetailsView}
<Table sortable={sortingCallbacks}
tableId={tableId} getTableRow={this.getRow}
virtual={virtual} items={items}
selectable={hasDetailsView} selectedItemId={selectedItemId}
sortable={sortingCallbacks} noItems={this.renderNoItems()}
getTableRow={this.getRow} className={classNames}
items={items} {...tableProps}
selectedItemId={selectedItemId} >
noItems={this.renderNoItems()} {this.renderTableHeader()}
{...({ {this.renderItems()}
...tableProps, </Table>
className: cssNames("box grow", tableProps.className, themeStore.activeTheme.type),
})}
>
{this.renderTableHeader()}
{
!virtual && items.map(item => this.getRow(item.getId()))
}
</Table>
)}
<AddRemoveButtons <AddRemoveButtons
onRemove={selectedItems.length ? removeItemsDialog : null} onRemove={selectedItems.length ? removeItemsDialog : null}
removeTooltip={`Remove selected items (${selectedItems.length})`} removeTooltip={`Remove selected items (${selectedItems.length})`}

View File

@ -13,6 +13,7 @@ export abstract class ItemStore<T extends ItemObject = ItemObject> {
protected defaultSorting = (item: T) => item.getName(); protected defaultSorting = (item: T) => item.getName();
@observable failedLoading = false;
@observable isLoading = false; @observable isLoading = false;
@observable isLoaded = false; @observable isLoaded = false;
@observable items = observable.array<T>([], { deep: false }); @observable items = observable.array<T>([], { deep: false });

View File

@ -146,18 +146,20 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
const items = await this.loadItems({ namespaces, api: this.api }); const items = await this.loadItems({ namespaces, api: this.api });
this.isLoaded = true;
if (merge) { if (merge) {
this.mergeItems(items, { replace: false }); this.mergeItems(items, { replace: false });
} else { } else {
this.mergeItems(items, { replace: true }); this.mergeItems(items, { replace: true });
} }
this.isLoaded = true;
this.failedLoading = false;
return items; return items;
} catch (error) { } catch (error) {
console.error("Loading store items failed", { error, store: this }); console.error("Loading store items failed", { error, store: this });
this.resetOnError(error); this.resetOnError(error);
this.failedLoading = true;
} finally { } finally {
this.isLoading = false; this.isLoading = false;
} }