mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Rollout support
Signed-off-by: vshakirova <vshakirova@mirantis.com>
This commit is contained in:
parent
d6d66682f6
commit
32320ecb6d
53
src/common/k8s-api/endpoints/controller-revision.api.ts
Normal file
53
src/common/k8s-api/endpoints/controller-revision.api.ts
Normal file
@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { WorkloadKubeObject } from "../workload-kube-object";
|
||||
import { autoBind } from "../../utils/autobind";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
|
||||
export class ControllerRevisionApi extends KubeApi<ControllerRevision> {
|
||||
|
||||
async getRevisions(params: { namespace: string; name: string }) {
|
||||
return await this.list({ namespace: params.namespace }, { labelSelector: `name=${params.name}` });
|
||||
}
|
||||
}
|
||||
|
||||
export class ControllerRevision extends WorkloadKubeObject {
|
||||
static kind = "ControllerRevision";
|
||||
static namespaced = true;
|
||||
static apiBase = "/apis/apps/v1/controllerrevisions";
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
declare revision: number;
|
||||
declare data: string;
|
||||
|
||||
getRevisionNumber() {
|
||||
return this.revision;
|
||||
}
|
||||
}
|
||||
|
||||
export const controllerRevisionApi = new ControllerRevisionApi({
|
||||
objectConstructor: ControllerRevision,
|
||||
});
|
||||
@ -22,11 +22,14 @@
|
||||
import get from "lodash/get";
|
||||
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
|
||||
import { autoBind } from "../../utils";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { metricsApi } from "./metrics.api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import type { IPodContainer, IPodMetrics } from "./pods.api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
import { WorkloadKubeApi } from "../../../renderer/components/+workloads/api/workload-kube-api";
|
||||
|
||||
export class DaemonSetApi extends WorkloadKubeApi<DaemonSet> {
|
||||
}
|
||||
|
||||
export class DaemonSet extends WorkloadKubeObject {
|
||||
static kind = "DaemonSet";
|
||||
@ -99,9 +102,6 @@ export class DaemonSet extends WorkloadKubeObject {
|
||||
}
|
||||
}
|
||||
|
||||
export class DaemonSetApi extends KubeApi<DaemonSet> {
|
||||
}
|
||||
|
||||
export function getMetricsForDaemonSets(daemonsets: DaemonSet[], namespace: string, selector = ""): Promise<IPodMetrics> {
|
||||
const podSelector = daemonsets.map(daemonset => `${daemonset.getName()}-[[:alnum:]]{5}`).join("|");
|
||||
const opts = { category: "pods", pods: podSelector, namespace, selector };
|
||||
|
||||
@ -19,17 +19,15 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import moment from "moment";
|
||||
|
||||
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
|
||||
import { autoBind } from "../../utils";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { metricsApi } from "./metrics.api";
|
||||
import type { IPodMetrics } from "./pods.api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
import { strategicMergePatch, WorkloadKubeApi } from "../../../renderer/components/+workloads/api/workload-kube-api";
|
||||
|
||||
export class DeploymentApi extends KubeApi<Deployment> {
|
||||
export class DeploymentApi extends WorkloadKubeApi<Deployment> {
|
||||
protected getScaleApiUrl(params: { namespace: string; name: string }) {
|
||||
return `${this.getUrl(params)}/scale`;
|
||||
}
|
||||
@ -51,21 +49,32 @@ export class DeploymentApi extends KubeApi<Deployment> {
|
||||
});
|
||||
}
|
||||
|
||||
restart(params: { namespace: string; name: string }) {
|
||||
pause(params: { namespace: string; name: string }) {
|
||||
return this.request.patch(this.getUrl(params), {
|
||||
data: {
|
||||
spec: {
|
||||
template: {
|
||||
metadata: {
|
||||
annotations: {"kubectl.kubernetes.io/restartedAt" : moment.utc().format()}
|
||||
}
|
||||
}
|
||||
paused: true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"content-type": "application/strategic-merge-patch+json"
|
||||
"content-type": strategicMergePatch
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
resume(params: { namespace: string; name: string }) {
|
||||
return this.request.patch(this.getUrl(params), {
|
||||
data: {
|
||||
spec: {
|
||||
paused: false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"content-type": strategicMergePatch
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -117,6 +126,7 @@ export class Deployment extends WorkloadKubeObject {
|
||||
}
|
||||
|
||||
declare spec: {
|
||||
paused?: boolean;
|
||||
replicas: number;
|
||||
selector: { matchLabels: { [app: string]: string } };
|
||||
template: {
|
||||
@ -231,6 +241,10 @@ export class Deployment extends WorkloadKubeObject {
|
||||
getReplicas() {
|
||||
return this.spec.replicas || 0;
|
||||
}
|
||||
|
||||
isPaused() {
|
||||
return this.spec?.paused || false;
|
||||
}
|
||||
}
|
||||
|
||||
let deploymentApi: DeploymentApi;
|
||||
|
||||
@ -66,6 +66,9 @@ export function getMetricsForReplicaSets(replicasets: ReplicaSet[], namespace: s
|
||||
});
|
||||
}
|
||||
|
||||
const changeCauseAnnotation = "kubernetes.io/change-cause";
|
||||
const revisionAnnotation = "deployment.kubernetes.io/revision";
|
||||
|
||||
export class ReplicaSet extends WorkloadKubeObject {
|
||||
static kind = "ReplicaSet";
|
||||
static namespaced = true;
|
||||
@ -117,6 +120,14 @@ export class ReplicaSet extends WorkloadKubeObject {
|
||||
return this.status.readyReplicas || 0;
|
||||
}
|
||||
|
||||
getRevisionNumber(): number {
|
||||
return parseInt(this.metadata.annotations[revisionAnnotation]);
|
||||
}
|
||||
|
||||
getChangeCause() {
|
||||
return this.metadata.annotations[changeCauseAnnotation] || "";
|
||||
}
|
||||
|
||||
getImages() {
|
||||
const containers: IPodContainer[] = get(this, "spec.template.spec.containers", []);
|
||||
|
||||
|
||||
@ -21,13 +21,13 @@
|
||||
|
||||
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
|
||||
import { autoBind } from "../../utils";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { metricsApi } from "./metrics.api";
|
||||
import type { IPodMetrics } from "./pods.api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
import { WorkloadKubeApi } from "../../../renderer/components/+workloads/api/workload-kube-api";
|
||||
|
||||
export class StatefulSetApi extends KubeApi<StatefulSet> {
|
||||
export class StatefulSetApi extends WorkloadKubeApi<StatefulSet> {
|
||||
protected getScaleApiUrl(params: { namespace: string; name: string }) {
|
||||
return `${this.getUrl(params)}/scale`;
|
||||
}
|
||||
|
||||
31
src/common/routes/controllerrevisions.ts
Normal file
31
src/common/routes/controllerrevisions.ts
Normal file
@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import type { RouteProps } from "react-router";
|
||||
import { buildURL } from "../utils/buildUrl";
|
||||
|
||||
export const controllerRevisionRoute: RouteProps = {
|
||||
path: "/controllerrevisions"
|
||||
};
|
||||
|
||||
export interface ControllerRevisionRouteParams {
|
||||
}
|
||||
|
||||
export const controllerRevisionURL = buildURL<ControllerRevisionRouteParams>(controllerRevisionRoute.path);
|
||||
@ -34,12 +34,15 @@ import type { KubeObjectDetailsProps } from "../kube-object-details";
|
||||
import { DaemonSet, getMetricsForDaemonSets, IPodMetrics } from "../../../common/k8s-api/endpoints";
|
||||
import { ResourceMetrics, ResourceMetricsText } from "../resource-metrics";
|
||||
import { PodCharts, podMetricTabs } from "../+workloads-pods/pod-charts";
|
||||
import { makeObservable, observable, reaction } from "mobx";
|
||||
import { autorun, makeObservable, observable, reaction } from "mobx";
|
||||
import { PodDetailsList } from "../+workloads-pods/pod-details-list";
|
||||
import { KubeObjectMeta } from "../kube-object-meta";
|
||||
import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
|
||||
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
||||
import { boundMethod } from "../../utils";
|
||||
import { RevisionHistory } from "../+workloads/revision-history";
|
||||
import type { ControllerRevision } from "../../../common/k8s-api/endpoints/controller-revision.api";
|
||||
import { controllerRevisionApi } from "../../../common/k8s-api/endpoints/controller-revision.api";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<DaemonSet> {
|
||||
}
|
||||
@ -47,6 +50,7 @@ interface Props extends KubeObjectDetailsProps<DaemonSet> {
|
||||
@observer
|
||||
export class DaemonSetDetails extends React.Component<Props> {
|
||||
@observable metrics: IPodMetrics = null;
|
||||
@observable revisionHistory: ControllerRevision[] = observable.array<ControllerRevision>();
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
@ -58,6 +62,17 @@ export class DaemonSetDetails extends React.Component<Props> {
|
||||
this.metrics = null;
|
||||
});
|
||||
|
||||
@disposeOnUnmount
|
||||
revisionLoader = autorun(async () => {
|
||||
const { object: daemonSet } = this.props;
|
||||
const revisionHistory = await controllerRevisionApi.getRevisions({
|
||||
name: daemonSet.getName(),
|
||||
namespace: daemonSet.getNs()
|
||||
}) as ControllerRevision[];
|
||||
|
||||
this.revisionHistory = observable.array(revisionHistory);
|
||||
});
|
||||
|
||||
componentDidMount() {
|
||||
podsStore.reloadAll();
|
||||
}
|
||||
@ -121,6 +136,7 @@ export class DaemonSetDetails extends React.Component<Props> {
|
||||
<PodDetailsStatuses pods={childPods}/>
|
||||
</DrawerItem>
|
||||
<ResourceMetricsText metrics={this.metrics}/>
|
||||
<RevisionHistory object={this.revisionHistory}/>
|
||||
<PodDetailsList pods={childPods} owner={daemonSet}/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -33,6 +33,8 @@ import { KubeObjectListLayout } from "../kube-object-list-layout";
|
||||
import { Badge } from "../badge";
|
||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||
import type { DaemonSetsRouteParams } from "../../../common/routes";
|
||||
import type { KubeObjectMenuProps } from "../kube-object-menu/kube-object-menu";
|
||||
import { RolloutMenu } from "../+workloads/rollout-menu";
|
||||
|
||||
enum columnId {
|
||||
name = "name",
|
||||
@ -91,7 +93,18 @@ export class DaemonSets extends React.Component<Props> {
|
||||
this.renderNodeSelector(daemonSet),
|
||||
daemonSet.getAge(),
|
||||
]}
|
||||
renderItemMenu={item => <DaemonSetMenu object={item} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function DaemonSetMenu(props: KubeObjectMenuProps<DaemonSet>) {
|
||||
const { object } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<RolloutMenu workloadKubeObject={object}/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -37,11 +37,10 @@ import { PodCharts, podMetricTabs } from "../+workloads-pods/pod-charts";
|
||||
import { makeObservable, observable, reaction } from "mobx";
|
||||
import { PodDetailsList } from "../+workloads-pods/pod-details-list";
|
||||
import { KubeObjectMeta } from "../kube-object-meta";
|
||||
import { replicaSetStore } from "../+workloads-replicasets/replicasets.store";
|
||||
import { DeploymentReplicaSets } from "./deployment-replicasets";
|
||||
import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
|
||||
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
||||
import { boundMethod } from "../../utils";
|
||||
import { DeploymentRevisionHistory } from "./deployment-revision-history";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<Deployment> {
|
||||
}
|
||||
@ -62,7 +61,6 @@ export class DeploymentDetails extends React.Component<Props> {
|
||||
|
||||
componentDidMount() {
|
||||
podsStore.reloadAll();
|
||||
replicaSetStore.reloadAll();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
@ -80,7 +78,7 @@ export class DeploymentDetails extends React.Component<Props> {
|
||||
const nodeSelector = deployment.getNodeSelectors();
|
||||
const selectors = deployment.getSelectors();
|
||||
const childPods = deploymentStore.getChildPods(deployment);
|
||||
const replicaSets = replicaSetStore.getReplicaSetsByOwner(deployment);
|
||||
const replicaSets = deploymentStore.getRelatedReplicas(deployment);
|
||||
const isMetricHidden = getActiveClusterEntity()?.isMetricHidden(ClusterMetricsResourceType.Deployment);
|
||||
|
||||
return (
|
||||
@ -143,7 +141,7 @@ export class DeploymentDetails extends React.Component<Props> {
|
||||
<PodDetailsTolerations workload={deployment}/>
|
||||
<PodDetailsAffinities workload={deployment}/>
|
||||
<ResourceMetricsText metrics={this.metrics}/>
|
||||
<DeploymentReplicaSets replicaSets={replicaSets}/>
|
||||
<DeploymentRevisionHistory object={replicaSets}/>
|
||||
<PodDetailsList pods={childPods} owner={deployment}/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,36 +0,0 @@
|
||||
.DeploymentDetails {
|
||||
.ReplicaSets {
|
||||
position: relative;
|
||||
min-height: 80px;
|
||||
|
||||
.Table {
|
||||
margin: 0 (-$margin * 3);
|
||||
}
|
||||
|
||||
.TableCell {
|
||||
&:first-child {
|
||||
margin-left: $margin;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: $margin;
|
||||
}
|
||||
|
||||
&.name {
|
||||
flex-grow: 2;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
@include table-cell-warning;
|
||||
}
|
||||
|
||||
&.namespace {
|
||||
flex-grow: 1.2;
|
||||
}
|
||||
|
||||
&.actions {
|
||||
@include table-cell-action;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
.RevisionHistory {
|
||||
position: relative;
|
||||
min-height: 80px;
|
||||
|
||||
.Table {
|
||||
margin: 0 (-$margin * 3);
|
||||
}
|
||||
|
||||
.TableCell {
|
||||
&:first-child {
|
||||
margin-left: $margin;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: $margin;
|
||||
}
|
||||
|
||||
&.revision {
|
||||
flex-grow: 0.5;
|
||||
}
|
||||
|
||||
&.name {
|
||||
flex-grow: 2;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
@include table-cell-warning;
|
||||
}
|
||||
|
||||
&.namespace {
|
||||
flex-grow: 1.2;
|
||||
}
|
||||
|
||||
&.actions {
|
||||
@include table-cell-action;
|
||||
}
|
||||
|
||||
&.changeCause:empty:after {
|
||||
content: '—';
|
||||
flex-grow: 1.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -19,89 +19,90 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import "./deployment-replicasets.scss";
|
||||
import "./deployment-revision-history.scss";
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import type { ReplicaSet } from "../../../common/k8s-api/endpoints";
|
||||
import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object-menu";
|
||||
import { Spinner } from "../spinner";
|
||||
import { prevDefault, stopPropagation } from "../../utils";
|
||||
import { DrawerTitle } from "../drawer";
|
||||
import { Table, TableCell, TableHead, TableRow } from "../table";
|
||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||
import { replicaSetStore } from "../+workloads-replicasets/replicasets.store";
|
||||
import { showDetails } from "../kube-detail-params";
|
||||
|
||||
import { prevDefault } from "../../utils/prevDefault";
|
||||
import { showDetails } from "../kube-detail-params/params";
|
||||
import { Badge } from "../badge/badge";
|
||||
|
||||
enum sortBy {
|
||||
name = "name",
|
||||
namespace = "namespace",
|
||||
pods = "pods",
|
||||
age = "age",
|
||||
revision = "revision",
|
||||
}
|
||||
|
||||
interface Props {
|
||||
replicaSets: ReplicaSet[];
|
||||
object: ReplicaSet[];
|
||||
}
|
||||
|
||||
@observer
|
||||
export class DeploymentReplicaSets extends React.Component<Props> {
|
||||
export class DeploymentRevisionHistory extends React.Component<Props> {
|
||||
private sortingCallbacks = {
|
||||
[sortBy.name]: (replicaSet: ReplicaSet) => replicaSet.getName(),
|
||||
[sortBy.namespace]: (replicaSet: ReplicaSet) => replicaSet.getNs(),
|
||||
[sortBy.age]: (replicaSet: ReplicaSet) => replicaSet.metadata.creationTimestamp,
|
||||
[sortBy.pods]: (replicaSet: ReplicaSet) => this.getPodsLength(replicaSet),
|
||||
[sortBy.name]: (object: ReplicaSet) => object.getName(),
|
||||
[sortBy.namespace]: (object: ReplicaSet) => object.getNs(),
|
||||
[sortBy.age]: (object: ReplicaSet) => object.metadata.creationTimestamp,
|
||||
[sortBy.revision]: (object: ReplicaSet) => object.getRevisionNumber(),
|
||||
};
|
||||
|
||||
getPodsLength(replicaSet: ReplicaSet) {
|
||||
return replicaSetStore.getChildPods(replicaSet).length;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { replicaSets } = this.props;
|
||||
const { object } = this.props;
|
||||
|
||||
if (!replicaSets.length && !replicaSetStore.isLoaded) return (
|
||||
<div className="ReplicaSets"><Spinner center/></div>
|
||||
);
|
||||
if (!replicaSets.length) return null;
|
||||
if (!object.length) return null;
|
||||
|
||||
return (
|
||||
<div className="ReplicaSets flex column">
|
||||
<DrawerTitle title="Deploy Revisions"/>
|
||||
<div className="RevisionHistory flex column">
|
||||
<DrawerTitle title="Revision history"/>
|
||||
<Table
|
||||
tableId="revision_history"
|
||||
selectable
|
||||
scrollable={false}
|
||||
sortable={this.sortingCallbacks}
|
||||
sortByDefault={{ sortBy: sortBy.pods, orderBy: "desc" }}
|
||||
sortByDefault={{ sortBy: sortBy.revision, orderBy: "desc" }}
|
||||
sortSyncWithUrl={false}
|
||||
className="box grow"
|
||||
>
|
||||
<TableHead>
|
||||
<TableCell className="revision" sortBy={sortBy.revision}>Revision</TableCell>
|
||||
<TableCell className="name" sortBy={sortBy.name}>Name</TableCell>
|
||||
<TableCell className="warning"/>
|
||||
<TableCell className="namespace" sortBy={sortBy.namespace}>Namespace</TableCell>
|
||||
<TableCell className="pods" sortBy={sortBy.pods}>Pods</TableCell>
|
||||
<TableCell className="changeCause">Change cause</TableCell>
|
||||
<TableCell className="age" sortBy={sortBy.age}>Age</TableCell>
|
||||
<TableCell className="actions"/>
|
||||
</TableHead>
|
||||
{
|
||||
replicaSets.map(replica => {
|
||||
object.map(object => {
|
||||
return (
|
||||
<TableRow
|
||||
key={replica.getId()}
|
||||
sortItem={replica}
|
||||
key={object.getId()}
|
||||
sortItem={object}
|
||||
nowrap
|
||||
onClick={prevDefault(() => showDetails(replica.selfLink, false))}
|
||||
onClick={prevDefault(() => showDetails(object.selfLink, false))}
|
||||
>
|
||||
<TableCell className="name">{replica.getName()}</TableCell>
|
||||
<TableCell className="warning"><KubeObjectStatusIcon key="icon" object={replica}/></TableCell>
|
||||
<TableCell className="namespace">{replica.getNs()}</TableCell>
|
||||
<TableCell className="pods">{this.getPodsLength(replica)}</TableCell>
|
||||
<TableCell className="age">{replica.getAge()}</TableCell>
|
||||
<TableCell className="actions" onClick={stopPropagation}>
|
||||
<ReplicaSetMenu object={replica}/>
|
||||
<TableCell className="revision">{object.getRevisionNumber()}</TableCell>
|
||||
<TableCell className="name">{object.getName()}</TableCell>
|
||||
<TableCell className="warning"><KubeObjectStatusIcon key="icon" object={object}/></TableCell>
|
||||
<TableCell className="namespace">{object.getNs()}</TableCell>
|
||||
<TableCell className="changeCause">{object.getChangeCause() &&
|
||||
<Badge
|
||||
key={object.getChangeCause()}
|
||||
label={object.getChangeCause()}
|
||||
tooltip={(
|
||||
<>
|
||||
<p>{object.getChangeCause()}</p>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
</TableCell>
|
||||
<TableCell className="age">{object.getAge()}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
@ -111,9 +112,3 @@ export class DeploymentReplicaSets extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function ReplicaSetMenu(props: KubeObjectMenuProps<ReplicaSet>) {
|
||||
return (
|
||||
<KubeObjectMenu {...props}/>
|
||||
);
|
||||
}
|
||||
@ -112,6 +112,7 @@ const dummyDeployment: Deployment = {
|
||||
toPlainObject: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
isPaused: jest.fn(),
|
||||
};
|
||||
|
||||
describe("<DeploymentScaleDialog />", () => {
|
||||
|
||||
@ -25,6 +25,7 @@ import { apiManager } from "../../../common/k8s-api/api-manager";
|
||||
import { Deployment, deploymentApi, PodStatus } from "../../../common/k8s-api/endpoints";
|
||||
import { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
|
||||
import { autoBind } from "../../utils";
|
||||
import { replicaSetStore } from "../+workloads-replicasets/replicasets.store";
|
||||
|
||||
export class DeploymentStore extends KubeObjectStore<Deployment> {
|
||||
api = deploymentApi;
|
||||
@ -67,6 +68,10 @@ export class DeploymentStore extends KubeObjectStore<Deployment> {
|
||||
.getByLabel(deployment.getTemplateLabels())
|
||||
.filter(pod => pod.getNs() === deployment.getNs());
|
||||
}
|
||||
|
||||
getRelatedReplicas(deployment: Deployment) {
|
||||
return replicaSetStore.getReplicaSetsByOwner(deployment);
|
||||
}
|
||||
}
|
||||
|
||||
export const deploymentStore = new DeploymentStore();
|
||||
|
||||
@ -24,12 +24,11 @@ import "./deployments.scss";
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import type { RouteComponentProps } from "react-router";
|
||||
import { Deployment, deploymentApi } from "../../../common/k8s-api/endpoints";
|
||||
import type { Deployment } from "../../../common/k8s-api/endpoints";
|
||||
import type { KubeObjectMenuProps } from "../kube-object-menu";
|
||||
import { MenuItem } from "../menu";
|
||||
import { Icon } from "../icon";
|
||||
import { DeploymentScaleDialog } from "./deployment-scale-dialog";
|
||||
import { ConfirmDialog } from "../confirm-dialog";
|
||||
import { deploymentStore } from "./deployments.store";
|
||||
import { replicaSetStore } from "../+workloads-replicasets/replicasets.store";
|
||||
import { podsStore } from "../+workloads-pods/pods.store";
|
||||
@ -40,8 +39,8 @@ import { cssNames } from "../../utils";
|
||||
import kebabCase from "lodash/kebabCase";
|
||||
import orderBy from "lodash/orderBy";
|
||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||
import { Notifications } from "../notifications";
|
||||
import type { DeploymentsRouteParams } from "../../../common/routes";
|
||||
import { RolloutMenu } from "../+workloads/rollout-menu";
|
||||
|
||||
enum columnId {
|
||||
name = "name",
|
||||
@ -125,28 +124,7 @@ export function DeploymentMenu(props: KubeObjectMenuProps<Deployment>) {
|
||||
<Icon material="open_with" tooltip="Scale" interactive={toolbar}/>
|
||||
<span className="title">Scale</span>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => ConfirmDialog.open({
|
||||
ok: async () =>
|
||||
{
|
||||
try {
|
||||
await deploymentApi.restart({
|
||||
namespace: object.getNs(),
|
||||
name: object.getName(),
|
||||
});
|
||||
} catch (err) {
|
||||
Notifications.error(err);
|
||||
}
|
||||
},
|
||||
labelOk: `Restart`,
|
||||
message: (
|
||||
<p>
|
||||
Are you sure you want to restart deployment <b>{object.getName()}</b>?
|
||||
</p>
|
||||
),
|
||||
})}>
|
||||
<Icon material="autorenew" tooltip="Restart" interactive={toolbar}/>
|
||||
<span className="title">Restart</span>
|
||||
</MenuItem>
|
||||
<RolloutMenu workloadKubeObject={object}/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -107,6 +107,8 @@ const dummyReplicaSet: ReplicaSet = {
|
||||
toPlainObject: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
getChangeCause: jest.fn(),
|
||||
getRevisionNumber: jest.fn(),
|
||||
};
|
||||
|
||||
describe("<ReplicaSetScaleDialog />", () => {
|
||||
|
||||
@ -23,7 +23,7 @@ import "./statefulset-details.scss";
|
||||
|
||||
import React from "react";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { makeObservable, observable, reaction } from "mobx";
|
||||
import { autorun, makeObservable, observable, reaction } from "mobx";
|
||||
import { Badge } from "../badge";
|
||||
import { DrawerItem } from "../drawer";
|
||||
import { PodDetailsStatuses } from "../+workloads-pods/pod-details-statuses";
|
||||
@ -40,6 +40,9 @@ import { KubeObjectMeta } from "../kube-object-meta";
|
||||
import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
|
||||
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
||||
import { boundMethod } from "../../utils";
|
||||
import { RevisionHistory } from "../+workloads/revision-history";
|
||||
import type { ControllerRevision } from "../../../common/k8s-api/endpoints/controller-revision.api";
|
||||
import { controllerRevisionApi } from "../../../common/k8s-api/endpoints/controller-revision.api";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<StatefulSet> {
|
||||
}
|
||||
@ -47,6 +50,7 @@ interface Props extends KubeObjectDetailsProps<StatefulSet> {
|
||||
@observer
|
||||
export class StatefulSetDetails extends React.Component<Props> {
|
||||
@observable metrics: IPodMetrics = null;
|
||||
@observable revisionHistory: ControllerRevision[] = observable.array<ControllerRevision>();
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
@ -58,6 +62,17 @@ export class StatefulSetDetails extends React.Component<Props> {
|
||||
this.metrics = null;
|
||||
});
|
||||
|
||||
@disposeOnUnmount
|
||||
revisionLoader = autorun(async () => {
|
||||
const { object: statefulSet } = this.props;
|
||||
const revisionHistory = await controllerRevisionApi.getRevisions({
|
||||
name: statefulSet.getName(),
|
||||
namespace: statefulSet.getNs()
|
||||
}) as ControllerRevision[];
|
||||
|
||||
this.revisionHistory = observable.array(revisionHistory);
|
||||
});
|
||||
|
||||
componentDidMount() {
|
||||
podsStore.reloadAll();
|
||||
}
|
||||
@ -119,6 +134,7 @@ export class StatefulSetDetails extends React.Component<Props> {
|
||||
<PodDetailsStatuses pods={childPods}/>
|
||||
</DrawerItem>
|
||||
<ResourceMetricsText metrics={this.metrics}/>
|
||||
<RevisionHistory object={this.revisionHistory}/>
|
||||
<PodDetailsList pods={childPods} owner={statefulSet}/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -32,10 +32,11 @@ import { eventStore } from "../+events/event.store";
|
||||
import type { KubeObjectMenuProps } from "../kube-object-menu";
|
||||
import { KubeObjectListLayout } from "../kube-object-list-layout";
|
||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||
import { StatefulSetScaleDialog } from "./statefulset-scale-dialog";
|
||||
import { MenuItem } from "../menu/menu";
|
||||
import { Icon } from "../icon/icon";
|
||||
import type { StatefulSetsRouteParams } from "../../../common/routes";
|
||||
import { MenuItem } from "../menu/menu";
|
||||
import { StatefulSetScaleDialog } from "./statefulset-scale-dialog";
|
||||
import { Icon } from "../icon/icon";
|
||||
import { RolloutMenu } from "../+workloads/rollout-menu";
|
||||
|
||||
enum columnId {
|
||||
name = "name",
|
||||
@ -89,7 +90,7 @@ export class StatefulSets extends React.Component<Props> {
|
||||
<KubeObjectStatusIcon key="icon" object={statefulSet}/>,
|
||||
statefulSet.getAge(),
|
||||
]}
|
||||
renderItemMenu={item => <StatefulSetMenu object={item} />}
|
||||
renderItemMenu={item => <StatefulSetMenu object={item}/>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -104,6 +105,7 @@ export function StatefulSetMenu(props: KubeObjectMenuProps<StatefulSet>) {
|
||||
<Icon material="open_with" tooltip="Scale" interactive={toolbar}/>
|
||||
<span className="title">Scale</span>
|
||||
</MenuItem>
|
||||
<RolloutMenu workloadKubeObject={object}/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
50
src/renderer/components/+workloads/api/workload-kube-api.ts
Normal file
50
src/renderer/components/+workloads/api/workload-kube-api.ts
Normal file
@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import moment from "moment";
|
||||
import { KubeApi } from "../../../../common/k8s-api/kube-api";
|
||||
import type { WorkloadKubeObject } from "../../../../common/k8s-api/workload-kube-object";
|
||||
|
||||
export const strategicMergePatch = "application/strategic-merge-patch+json";
|
||||
export const jsonPatch = "application/json-patch+json";
|
||||
|
||||
export abstract class WorkloadKubeApi<T extends WorkloadKubeObject> extends KubeApi<T> {
|
||||
//TODO move scale api
|
||||
|
||||
restart(params: { namespace: string; name: string }) {
|
||||
return this.request.patch(this.getUrl(params), {
|
||||
data: {
|
||||
spec: {
|
||||
template: {
|
||||
metadata: {
|
||||
annotations: { "kubectl.kubernetes.io/restartedAt": moment.utc().format() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"content-type": strategicMergePatch
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
55
src/renderer/components/+workloads/revision-history.scss
Normal file
55
src/renderer/components/+workloads/revision-history.scss
Normal file
@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
.RevisionHistory {
|
||||
position: relative;
|
||||
min-height: 80px;
|
||||
|
||||
.Table {
|
||||
margin: 0 (-$margin * 3);
|
||||
}
|
||||
|
||||
.TableCell {
|
||||
&:first-child {
|
||||
margin-left: $margin;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: $margin;
|
||||
}
|
||||
|
||||
&.name {
|
||||
flex-grow: 2;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
@include table-cell-warning;
|
||||
}
|
||||
|
||||
&.namespace {
|
||||
flex-grow: 1.2;
|
||||
}
|
||||
|
||||
&.actions {
|
||||
@include table-cell-action;
|
||||
}
|
||||
}
|
||||
}
|
||||
97
src/renderer/components/+workloads/revision-history.tsx
Normal file
97
src/renderer/components/+workloads/revision-history.tsx
Normal file
@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import "./revision-history.scss";
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { DrawerTitle } from "../drawer";
|
||||
import { Table, TableCell, TableHead, TableRow } from "../table";
|
||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||
import type { ControllerRevision } from "../../../common/k8s-api/endpoints/controller-revision.api";
|
||||
|
||||
enum sortBy {
|
||||
name = "name",
|
||||
namespace = "namespace",
|
||||
pods = "pods",
|
||||
age = "age",
|
||||
revision = "revision",
|
||||
}
|
||||
|
||||
interface Props {
|
||||
object: ControllerRevision[];
|
||||
}
|
||||
|
||||
@observer
|
||||
export class RevisionHistory extends React.Component<Props> {
|
||||
private sortingCallbacks = {
|
||||
[sortBy.name]: (object: ControllerRevision) => object.getName(),
|
||||
[sortBy.namespace]: (object: ControllerRevision) => object.getNs(),
|
||||
[sortBy.age]: (object: ControllerRevision) => object.metadata.creationTimestamp,
|
||||
[sortBy.revision]: (object: ControllerRevision) => object.getRevisionNumber(),
|
||||
};
|
||||
|
||||
render() {
|
||||
const { object } = this.props;
|
||||
|
||||
if (!object.length) return null;
|
||||
|
||||
return (
|
||||
<div className="RevisionHistory flex column">
|
||||
<DrawerTitle title="Revision history"/>
|
||||
<Table
|
||||
tableId="revision_history"
|
||||
selectable
|
||||
scrollable={false}
|
||||
sortable={this.sortingCallbacks}
|
||||
sortByDefault={{ sortBy: sortBy.revision, orderBy: "desc" }}
|
||||
sortSyncWithUrl={false}
|
||||
className="box grow"
|
||||
>
|
||||
<TableHead>
|
||||
<TableCell className="revision" sortBy={sortBy.revision}>Revision</TableCell>
|
||||
<TableCell className="name" sortBy={sortBy.name}>Name</TableCell>
|
||||
<TableCell className="warning"/>
|
||||
<TableCell className="namespace" sortBy={sortBy.namespace}>Namespace</TableCell>
|
||||
<TableCell className="age" sortBy={sortBy.age}>Age</TableCell>
|
||||
</TableHead>
|
||||
{
|
||||
object.map(object => {
|
||||
return (
|
||||
<TableRow
|
||||
key={object.getId()}
|
||||
sortItem={object}
|
||||
nowrap
|
||||
>
|
||||
<TableCell className="revision">{object.getRevisionNumber()}</TableCell>
|
||||
<TableCell className="name">{object.getName()}</TableCell>
|
||||
<TableCell className="warning"><KubeObjectStatusIcon key="icon" object={object}/></TableCell>
|
||||
<TableCell className="namespace">{object.getNs()}</TableCell>
|
||||
<TableCell className="age">{object.getAge()}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
}
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
146
src/renderer/components/+workloads/rollout-menu.tsx
Normal file
146
src/renderer/components/+workloads/rollout-menu.tsx
Normal file
@ -0,0 +1,146 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { MenuItem } from "../menu";
|
||||
import { ConfirmDialog } from "../confirm-dialog";
|
||||
import { Notifications } from "../notifications";
|
||||
import { Icon } from "../icon";
|
||||
import { WorkloadRollbackDialog } from "./workload-rollback-dialog";
|
||||
import type { WorkloadKubeObject } from "../../../common/k8s-api/workload-kube-object";
|
||||
import { apiManager } from "../../../common/k8s-api/api-manager";
|
||||
import { Deployment, deploymentApi } from "../../../common/k8s-api/endpoints/deployment.api";
|
||||
|
||||
interface Props {
|
||||
workloadKubeObject: WorkloadKubeObject;
|
||||
}
|
||||
|
||||
export function renderRestart(workloadKubeObject: WorkloadKubeObject) {
|
||||
const api: any = apiManager.getApiByKind(workloadKubeObject.kind, workloadKubeObject.apiVersion);
|
||||
|
||||
return (
|
||||
<MenuItem onClick={() => ConfirmDialog.open({
|
||||
ok: async () => {
|
||||
try {
|
||||
await api.restart({
|
||||
namespace: workloadKubeObject.getNs(),
|
||||
name: workloadKubeObject.getName(),
|
||||
});
|
||||
} catch (err) {
|
||||
Notifications.error(err);
|
||||
}
|
||||
},
|
||||
labelOk: `Restart`,
|
||||
message: (
|
||||
<p>
|
||||
Are you sure you want to restart {workloadKubeObject.kind} <b>{workloadKubeObject.getName()}</b>?
|
||||
</p>
|
||||
),
|
||||
})}>
|
||||
<Icon material="autorenew" tooltip="Restart" interactive={true}/>
|
||||
<span className="title">Restart</span>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
export function renderRollback(workloadKubeObject: WorkloadKubeObject) {
|
||||
|
||||
return (
|
||||
<MenuItem onClick={() => WorkloadRollbackDialog.open(workloadKubeObject)}>
|
||||
<Icon material="history" tooltip="Rollback" interactive={true}/>
|
||||
<span className="title">Rollback</span>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
export function renderPause(workloadKubeObject: Deployment) {
|
||||
|
||||
return (
|
||||
<MenuItem onClick={() => ConfirmDialog.open({
|
||||
ok: async () => {
|
||||
try {
|
||||
await deploymentApi.pause({ namespace: workloadKubeObject.getNs(), name: workloadKubeObject.getName() });
|
||||
} catch (err) {
|
||||
Notifications.error(err);
|
||||
}
|
||||
},
|
||||
labelOk: `Pause`,
|
||||
message: (
|
||||
<p>
|
||||
Pause Deployment <b>{workloadKubeObject.getName()}</b>?
|
||||
</p>),
|
||||
})}>
|
||||
<Icon material="pause_circle_filled" tooltip="Pause" interactive={true}/>
|
||||
<span className="title">Pause</span>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
export function renderResume(workloadKubeObject: Deployment) {
|
||||
|
||||
return (
|
||||
<MenuItem onClick={() => ConfirmDialog.open({
|
||||
ok: async () => {
|
||||
try {
|
||||
await deploymentApi.resume({ namespace: workloadKubeObject.getNs(), name: workloadKubeObject.getName() });
|
||||
} catch (err) {
|
||||
Notifications.error(err);
|
||||
}
|
||||
},
|
||||
labelOk: `Resume`,
|
||||
message: (
|
||||
<p>
|
||||
Resume Deployment <b>{workloadKubeObject.getName()}</b>?
|
||||
</p>),
|
||||
})}>
|
||||
<Icon material="play_circle_outline" tooltip="Resume" interactive={true}/>
|
||||
<span className="title">Resume</span>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
export function pauseResumeMenu(workloadKubeObject: Deployment) {
|
||||
|
||||
return (
|
||||
<>
|
||||
{!workloadKubeObject.isPaused() ? renderPause(workloadKubeObject) : renderResume(workloadKubeObject)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export class RolloutMenu extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { workloadKubeObject } = this.props;
|
||||
|
||||
if (!workloadKubeObject) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{workloadKubeObject instanceof Deployment && pauseResumeMenu(workloadKubeObject)}
|
||||
{renderRestart(workloadKubeObject)}
|
||||
{renderRollback(workloadKubeObject)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
.WorkloadRollbackDialog {
|
||||
.WizardStep {
|
||||
text-align: center;
|
||||
|
||||
.step-content {
|
||||
padding: var(--wizard-spacing);
|
||||
}
|
||||
}
|
||||
}
|
||||
174
src/renderer/components/+workloads/workload-rollback-dialog.tsx
Normal file
174
src/renderer/components/+workloads/workload-rollback-dialog.tsx
Normal file
@ -0,0 +1,174 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import "./workload-rollback-dialog.scss";
|
||||
|
||||
import type { DialogProps } from "../dialog/dialog";
|
||||
import React from "react";
|
||||
import { Select, SelectOption } from "../select/select";
|
||||
import type { ControllerRevision } from "../../../common/k8s-api/endpoints/controller-revision.api";
|
||||
import { Wizard, WizardStep } from "../wizard/wizard";
|
||||
import { Dialog } from "../dialog/dialog";
|
||||
import { createTerminalTab, TerminalStore } from "../dock/terminal.store";
|
||||
import type { WorkloadKubeObject } from "../../../common/k8s-api/workload-kube-object";
|
||||
import type { ReplicaSet } from "../../../common/k8s-api/endpoints/replica-set.api";
|
||||
import { makeObservable, observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import orderBy from "lodash/orderBy";
|
||||
import { Deployment } from "../../../common/k8s-api/endpoints/deployment.api";
|
||||
import { deploymentStore } from "../+workloads-deployments/deployments.store";
|
||||
import { controllerRevisionApi } from "../../../common/k8s-api/endpoints/controller-revision.api";
|
||||
|
||||
interface Props extends DialogProps {
|
||||
}
|
||||
|
||||
const dialogState = observable.object({
|
||||
isOpen: false,
|
||||
workloadKubeObject: null as WorkloadKubeObject,
|
||||
});
|
||||
|
||||
export interface KubeObjectHistory {
|
||||
revision: number;
|
||||
date: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class WorkloadRollbackDialog extends React.Component<Props> {
|
||||
|
||||
@observable workloadObject: WorkloadKubeObject;
|
||||
@observable isLoading = false;
|
||||
@observable revisions = observable.array<KubeObjectHistory>();
|
||||
@observable revision: KubeObjectHistory;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
static open(workloadKubeObject: WorkloadKubeObject) {
|
||||
dialogState.isOpen = true;
|
||||
dialogState.workloadKubeObject = workloadKubeObject;
|
||||
}
|
||||
|
||||
static close() {
|
||||
dialogState.isOpen = false;
|
||||
}
|
||||
|
||||
close = () => {
|
||||
WorkloadRollbackDialog.close();
|
||||
};
|
||||
|
||||
get workloadKubeObject(): WorkloadKubeObject {
|
||||
return dialogState.workloadKubeObject;
|
||||
}
|
||||
|
||||
getHistory(revisions: ReplicaSet[] | ControllerRevision[]) {
|
||||
return revisions.map(revision => ({
|
||||
revision: revision.getRevisionNumber(),
|
||||
date: new Date(revision.metadata.creationTimestamp).toLocaleString(),
|
||||
name: revision.getName(),
|
||||
} as KubeObjectHistory));
|
||||
}
|
||||
|
||||
onOpen = async () => {
|
||||
this.isLoading = true;
|
||||
let revisions: ReplicaSet[] | ControllerRevision[];
|
||||
|
||||
if(this.workloadKubeObject instanceof Deployment) {
|
||||
revisions = deploymentStore.getRelatedReplicas(this.workloadKubeObject);
|
||||
}
|
||||
else {
|
||||
revisions = await controllerRevisionApi.getRevisions({ namespace: this.workloadKubeObject.getNs(),
|
||||
name: this.workloadKubeObject.getName() });
|
||||
}
|
||||
|
||||
this.revisions = observable.array(orderBy(this.getHistory(revisions), "revision", "desc"));
|
||||
this.revision = this.revisions[0];
|
||||
this.isLoading = false;
|
||||
};
|
||||
|
||||
rollback = async () => {
|
||||
const shell = createTerminalTab({
|
||||
title: `Rollback: ${this.workloadKubeObject.getName()} (namespace: ${this.workloadKubeObject.getNs()})`
|
||||
});
|
||||
|
||||
TerminalStore.getInstance().sendCommand(`kubectl rollout undo ${this.workloadKubeObject.kind.toLocaleLowerCase()}` +
|
||||
`/${this.workloadKubeObject.getName()} --to-revision=${this.revision.revision} -n=${this.workloadKubeObject.getNs()}`,
|
||||
{
|
||||
enter: true,
|
||||
tabId: shell.id
|
||||
});
|
||||
|
||||
this.close();
|
||||
};
|
||||
|
||||
renderContent() {
|
||||
const { revision, revisions } = this;
|
||||
|
||||
if (revisions.length < 2) {
|
||||
return <p>No revisions to rollback.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex gaps align-center">
|
||||
<b>Revision</b>
|
||||
<Select
|
||||
themeName="light"
|
||||
value={revision}
|
||||
options={revisions}
|
||||
formatOptionLabel={({ value }: SelectOption<KubeObjectHistory>) => `${value.revision} - ${value.name},
|
||||
created: ${value.date}`}
|
||||
onChange={({ value }: SelectOption<KubeObjectHistory>) => this.revision = value}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { ...dialogProps } = this.props;
|
||||
const kubeObjectName = this.workloadKubeObject ? this.workloadKubeObject.getName() : "";
|
||||
const header = <h5>Rollback {this.workloadKubeObject?.kind.toLowerCase()} <b>{kubeObjectName}</b></h5>;
|
||||
const {revisions} = this;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
{...dialogProps}
|
||||
className="WorkloadRollbackDialog"
|
||||
isOpen={dialogState.isOpen}
|
||||
onOpen={this.onOpen}
|
||||
close={this.close}
|
||||
>
|
||||
<Wizard header={header} done={this.close}>
|
||||
<WizardStep
|
||||
scrollable={false}
|
||||
nextLabel="Rollback"
|
||||
next={this.rollback}
|
||||
loading={this.isLoading}
|
||||
disabledNext={revisions.length < 2}
|
||||
>
|
||||
{this.renderContent()}
|
||||
</WizardStep>
|
||||
</Wizard>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -73,6 +73,7 @@ import { getHostedClusterId } from "../utils";
|
||||
import { ClusterStore } from "../../common/cluster-store";
|
||||
import type { ClusterId } from "../../common/cluster-types";
|
||||
import { watchHistoryState } from "../remote-helpers/history-updater";
|
||||
import { WorkloadRollbackDialog } from "./+workloads/workload-rollback-dialog";
|
||||
|
||||
@observer
|
||||
export class App extends React.Component {
|
||||
@ -222,6 +223,7 @@ export class App extends React.Component {
|
||||
<StatefulSetScaleDialog/>
|
||||
<ReplicaSetScaleDialog/>
|
||||
<CronJobTriggerDialog/>
|
||||
<WorkloadRollbackDialog/>
|
||||
<CommandContainer clusterId={App.clusterId}/>
|
||||
</ErrorBoundary>
|
||||
</Router>
|
||||
|
||||
@ -25,6 +25,7 @@ import { CronJobMenu } from "../components/+workloads-cronjobs";
|
||||
import { DeploymentMenu } from "../components/+workloads-deployments";
|
||||
import { ReplicaSetMenu } from "../components/+workloads-replicasets";
|
||||
import { StatefulSetMenu } from "../components/+workloads-statefulsets";
|
||||
import { DaemonSetMenu } from "../components/+workloads-daemonsets";
|
||||
|
||||
export function initKubeObjectMenuRegistry() {
|
||||
KubeObjectMenuRegistry.getInstance()
|
||||
@ -63,6 +64,13 @@ export function initKubeObjectMenuRegistry() {
|
||||
components: {
|
||||
MenuItem: StatefulSetMenu
|
||||
}
|
||||
},
|
||||
{
|
||||
kind: "DaemonSet",
|
||||
apiVersions: ["apps/v1"],
|
||||
components: {
|
||||
MenuItem: DaemonSetMenu
|
||||
}
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user