Skip to content

Commit

Permalink
Merge #74408
Browse files Browse the repository at this point in the history
74408: cluster-ui: enable terminate session / query buttons r=matthewtodd a=matthewtodd

Addresses #70832.

The backend endpoints to support these actions were backported to the
release-21.2 branch in cbe3347 and first released in version 21.2.2:

```
$ git tag --contains cbe3347
@cockroachlabs/[email protected]
v21.2.2
v21.2.3
```

Cockroach Cloud UI code may selectively enable these buttons by setting
`pages.{sessions,sessionDetails}.showTerminateActions` in the UI Config
slice. The default is `false`, continuing to not show the buttons.

Release note (ui change): The terminate session and terminate query
buttons are again available to be enabled in the cluster ui.

Co-authored-by: Matthew Todd <[email protected]>
  • Loading branch information
craig[bot] and matthewtodd committed Jan 6, 2022
2 parents 1bea51a + 911fe5f commit 31a339c
Show file tree
Hide file tree
Showing 14 changed files with 268 additions and 12 deletions.
3 changes: 3 additions & 0 deletions pkg/ui/workspaces/cluster-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@
"@storybook/addon-links": "^6.1.21",
"@storybook/addons": "^6.1.21",
"@storybook/react": "^6.1.21",
"@testing-library/react": "^12.1.0",
"@testing-library/user-event": "^13.5.0",
"@testing-library/dom": "^8.11.1",
"@types/chai": "^4.2.11",
"@types/classnames": "^2.2.9",
"@types/d3": "<4.0.0",
Expand Down
85 changes: 85 additions & 0 deletions pkg/ui/workspaces/cluster-ui/src/sessions/sessionDetails.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright 2021 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

import React from "react";
import { createMemoryHistory } from "history";
import { MemoryRouter } from "react-router-dom";
import { render, screen } from "@testing-library/react";

import SessionDetails, { SessionDetailsProps } from "./sessionDetails";
import { activeSession } from "./sessionsPage.fixture";

describe("SessionDetails", () => {
const irrelevantProps: SessionDetailsProps = {
history: createMemoryHistory({ initialEntries: ["/sessions"] }),
location: {
pathname: "/sessions/blah",
search: "",
hash: "",
state: null,
},
match: {
path: "/sessions/blah",
url: "/sessions/blah",
isExact: true,
params: { session: "blah" },
},
nodeNames: {},
session: activeSession,
sessionError: null,
refreshSessions: () => {},
refreshNodes: () => {},
refreshNodesLiveness: () => {},
cancelSession: _ => {}, // eslint-disable-line @typescript-eslint/no-unused-vars
cancelQuery: _ => {}, // eslint-disable-line @typescript-eslint/no-unused-vars
};

const irrelevantUIConfig: Omit<
SessionDetailsProps["uiConfig"],
"showTerminateActions"
> = {
showGatewayNodeLink: true,
};

it("shows the cancel buttons by default", () => {
render(
<MemoryRouter>
<SessionDetails {...irrelevantProps} />
</MemoryRouter>,
);
expect(screen.queryByText("Cancel session")).not.toBeNull();
});

it("shows the cancel buttons when asked", () => {
render(
<MemoryRouter>
<SessionDetails
{...irrelevantProps}
uiConfig={{ showTerminateActions: true, ...irrelevantUIConfig }}
/>
,
</MemoryRouter>,
);
expect(screen.queryByText("Cancel session")).not.toBeNull();
});

it("hides the cancel buttons when asked", () => {
render(
<MemoryRouter>
<SessionDetails
{...irrelevantProps}
uiConfig={{ showTerminateActions: false, ...irrelevantUIConfig }}
/>
,
</MemoryRouter>,
);
expect(screen.queryByText("Cancel session")).toBeNull();
});
});
8 changes: 4 additions & 4 deletions pkg/ui/workspaces/cluster-ui/src/sessions/sessionDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ export interface OwnProps {
cancelQuery: (payload: ICancelQueryRequest) => void;
uiConfig?: UIConfigState["pages"]["sessionDetails"];
isTenant?: UIConfigState["isTenant"];
isCloud?: boolean;
onBackButtonClick?: () => void;
onTerminateSessionClick?: () => void;
onTerminateStatementClick?: () => void;
Expand Down Expand Up @@ -96,7 +95,7 @@ export class SessionDetails extends React.Component<SessionDetailsProps> {
terminateSessionRef: React.RefObject<TerminateSessionModalRef>;
terminateQueryRef: React.RefObject<TerminateQueryModalRef>;
static defaultProps = {
uiConfig: { showGatewayNodeLink: true },
uiConfig: { showGatewayNodeLink: true, showTerminateActions: true },
isTenant: false,
};

Expand Down Expand Up @@ -136,12 +135,13 @@ export class SessionDetails extends React.Component<SessionDetailsProps> {
sessionError,
cancelSession,
cancelQuery,
isCloud,
uiConfig,
onTerminateSessionClick,
onTerminateStatementClick,
} = this.props;
const session = this.props.session?.session;
const showActionButtons = !!session && !sessionError && !isCloud;
const showActionButtons =
!!session && !sessionError && uiConfig?.showTerminateActions;
return (
<div className={cx("sessions-details")}>
<Helmet title={`Details | ${sessionID} | Sessions`} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ export const SessionDetailsPageConnected = withRouter(
sessionError: state.adminUI.sessions.lastError,
uiConfig: selectSessionDetailsUiConfig(state),
isTenant: selectIsTenant(state),
isCloud: true,
}),
{
refreshSessions: sessionsActions.refresh,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const sessionDetailsPropsBase: SessionDetailsProps = {
refreshNodesLiveness: () => {},
uiConfig: {
showGatewayNodeLink: true,
showTerminateActions: true,
},
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ export const sessionsPagePropsFixture: SessionsPageProps = {
refreshSessions: () => {},
cancelSession: (req: CancelSessionRequestMessage) => {},
cancelQuery: (req: CancelQueryRequestMessage) => {},
uiConfig: { showTerminateActions: true },
onSortingChange: () => {},
};

Expand All @@ -191,5 +192,6 @@ export const sessionsPagePropsEmptyFixture: SessionsPageProps = {
refreshSessions: () => {},
cancelSession: (req: CancelSessionRequestMessage) => {},
cancelQuery: (req: CancelQueryRequestMessage) => {},
uiConfig: { showTerminateActions: true },
onSortingChange: () => {},
};
75 changes: 75 additions & 0 deletions pkg/ui/workspaces/cluster-ui/src/sessions/sessionsPage.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright 2021 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

import React from "react";
import { createMemoryHistory } from "history";
import { MemoryRouter } from "react-router-dom";
import { render, screen } from "@testing-library/react";

import SessionsPage, { SessionsPageProps } from "./sessionsPage";

describe("SessionsPage", () => {
const irrelevantProps: SessionsPageProps = {
history: createMemoryHistory({ initialEntries: ["/sessions"] }),
location: { pathname: "/sessions", search: "", hash: "", state: null },
match: { path: "/sessions", url: "/sessions", isExact: true, params: {} },
sessions: [],
sessionsError: null,
sortSetting: { ascending: false, columnTitle: "statementAge" },
refreshSessions: () => {},
cancelSession: _ => {}, // eslint-disable-line @typescript-eslint/no-unused-vars
cancelQuery: _ => {}, // eslint-disable-line @typescript-eslint/no-unused-vars
onSortingChange: () => {},
};

const baseColumnCount = 5;

it("shows the extra actions column by default", () => {
render(
<MemoryRouter>
<SessionsPage {...irrelevantProps} />
</MemoryRouter>,
);

expect(screen.getAllByRole("columnheader")).toHaveLength(
baseColumnCount + 1,
);
});

it("shows the extra actions column when asked", () => {
render(
<MemoryRouter>
<SessionsPage
uiConfig={{ showTerminateActions: true }}
{...irrelevantProps}
/>
,
</MemoryRouter>,
);

expect(screen.getAllByRole("columnheader")).toHaveLength(
baseColumnCount + 1,
);
});

it("hides the extra actions column when asked", () => {
render(
<MemoryRouter>
<SessionsPage
uiConfig={{ showTerminateActions: false }}
{...irrelevantProps}
/>
,
</MemoryRouter>,
);

expect(screen.getAllByRole("columnheader")).toHaveLength(baseColumnCount);
});
});
8 changes: 6 additions & 2 deletions pkg/ui/workspaces/cluster-ui/src/sessions/sessionsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import TerminateSessionModal, {
TerminateSessionModalRef,
} from "./terminateSessionModal";

import { UIConfigState } from "src/store";
import {
ICancelSessionRequest,
ICancelQueryRequest,
Expand All @@ -56,7 +57,7 @@ export interface OwnProps {
refreshSessions: () => void;
cancelSession: (payload: ICancelSessionRequest) => void;
cancelQuery: (payload: ICancelQueryRequest) => void;
isCloud?: boolean;
uiConfig?: UIConfigState["pages"]["sessions"];
onPageChanged?: (newPage: number) => void;
onSortingChange?: (
name: string,
Expand All @@ -80,6 +81,9 @@ export class SessionsPage extends React.Component<
> {
terminateSessionRef: React.RefObject<TerminateSessionModalRef>;
terminateQueryRef: React.RefObject<TerminateQueryModalRef>;
static defaultProps = {
uiConfig: { showTerminateActions: true },
};

constructor(props: SessionsPageProps) {
super(props);
Expand Down Expand Up @@ -172,7 +176,7 @@ export class SessionsPage extends React.Component<
columns={makeSessionsColumns(
this.terminateSessionRef,
this.terminateQueryRef,
this.props.isCloud,
this.props.uiConfig?.showTerminateActions,
this.props.onSessionClick,
this.props.onTerminateStatementClick,
this.props.onTerminateSessionClick,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright 2021 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

import assert from "assert";
import fetchMock from "jest-fetch-mock";
import { applyMiddleware, createStore, Store } from "redux";
import createSagaMiddleware from "redux-saga";

import { rootReducer, sagas } from "src/store";
import {
actions,
ICancelQueryRequest,
ICancelSessionRequest,
} from "src/store/terminateQuery";

class TestDriver {
private readonly store: Store;

constructor() {
const sagaMiddleware = createSagaMiddleware();
this.store = createStore(rootReducer, {}, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(sagas);
}

async cancelQuery(req: ICancelQueryRequest) {
return this.store.dispatch(actions.terminateQuery(req));
}

async cancelSession(req: ICancelSessionRequest) {
return this.store.dispatch(actions.terminateSession(req));
}
}

describe("SessionsPage Connections", () => {
beforeAll(fetchMock.enableMocks);
afterEach(fetchMock.resetMocks);
afterAll(fetchMock.disableMocks);

describe("cancelQuery", () => {
it("fires off an HTTP request", async () => {
const driver = new TestDriver();
assert.deepStrictEqual(fetchMock.mock.calls.length, 0);
await driver.cancelQuery({ node_id: "1" });
assert.deepStrictEqual(
fetchMock.mock.calls[0][0],
"/_status/cancel_query/1",
);
});
});

describe("cancelSession", () => {
it("fires off an HTTP request", async () => {
const driver = new TestDriver();
assert.deepStrictEqual(fetchMock.mock.calls.length, 0);
await driver.cancelSession({ node_id: "1" });
assert.deepStrictEqual(
fetchMock.mock.calls[0][0],
"/_status/cancel_session/1",
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ import { SessionsState } from "src/store/sessions";
import { createSelector } from "reselect";
import { SessionsPage } from "./index";

import { actions as sessionsActions } from "src/store/sessions";
import {
actions as sessionsActions,
selectSessionsUiConfig,
} from "src/store/sessions";
import { actions as localStorageActions } from "src/store/localStorage";
import {
actions as terminateQueryActions,
Expand Down Expand Up @@ -47,7 +50,7 @@ export const SessionsPageConnected = withRouter(
(state: AppState, props: RouteComponentProps) => ({
sessions: selectSessions(state),
sessionsError: state.adminUI.sessions.lastError,
isCloud: true,
uiConfig: selectSessionsUiConfig(state),
sortSetting: selectSortSetting(state),
}),
(dispatch: Dispatch) => ({
Expand Down
4 changes: 2 additions & 2 deletions pkg/ui/workspaces/cluster-ui/src/sessions/sessionsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ const StatementTableCell = (props: { session: ISession }) => {
export function makeSessionsColumns(
terminateSessionRef?: React.RefObject<TerminateSessionModalRef>,
terminateQueryRef?: React.RefObject<TerminateQueryModalRef>,
isCloud?: boolean,
enableTerminateActions?: boolean,
onSessionClick?: () => void,
onTerminateSessionClick?: () => void,
onTerminateStatementClick?: () => void,
Expand Down Expand Up @@ -246,5 +246,5 @@ export function makeSessionsColumns(
},
};

return isCloud ? columns : columns.concat([actions]);
return enableTerminateActions ? columns.concat([actions]) : columns;
}
Loading

0 comments on commit 31a339c

Please sign in to comment.