From 57fc667c7bcf0455b0836705e40f1043b5cd4224 Mon Sep 17 00:00:00 2001 From: Gerardo Torres Date: Fri, 22 Apr 2022 17:10:58 -0400 Subject: [PATCH] ui: display closed sessions, add username and session status filter Fixes #67888, #79914. Previously, the sessions page UI did not support displaying closed sessions and did not support the ability to filter by username or session status. This commit adds the "Closed" session status to closed sessions and adds the ability to filter by username and session status. Release note (ui change): sessions overview and session details pages now display closed sessions; sessions overview page now has username and session status filters --- .../cluster-ui/src/queryFilter/filter.tsx | 63 +++++++++++++++++++ .../src/sessions/sessionDetails.module.scss | 6 ++ .../src/sessions/sessionDetails.tsx | 16 +++-- .../sessions/sessionDetailsPage.fixture.ts | 6 ++ .../sessions/sessionDetailsPage.stories.tsx | 4 ++ .../src/sessions/sessionsPage.fixture.ts | 30 ++++++++- .../cluster-ui/src/sessions/sessionsPage.tsx | 35 +++++++++++ .../src/sessions/sessionsTable.module.scss | 6 ++ .../cluster-ui/src/sessions/sessionsTable.tsx | 31 +++++++-- 9 files changed, 181 insertions(+), 16 deletions(-) diff --git a/pkg/ui/workspaces/cluster-ui/src/queryFilter/filter.tsx b/pkg/ui/workspaces/cluster-ui/src/queryFilter/filter.tsx index 9401f2df04ad..5df6f2072918 100644 --- a/pkg/ui/workspaces/cluster-ui/src/queryFilter/filter.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/queryFilter/filter.tsx @@ -36,9 +36,13 @@ interface QueryFilter { activeFilters: number; filters: Filters; dbNames?: string[]; + usernames?: string[]; + sessionStatuses?: string[]; regions?: string[]; nodes?: string[]; showDB?: boolean; + showUsername?: boolean; + showSessionStatus?: boolean; showSqlType?: boolean; showScan?: boolean; showRegions?: boolean; @@ -64,6 +68,8 @@ export interface Filters extends Record { fullScan?: boolean; regions?: string; nodes?: string; + username?: string; + sessionStatus?: string; } const timeUnit = [ @@ -81,6 +87,8 @@ export const defaultFilters: Required = { database: "", regions: "", nodes: "", + username: "", + sessionStatus: "", }; // getFullFiltersObject returns Filters with every field defined as @@ -372,6 +380,8 @@ export class Filter extends React.Component { const { appNames, dbNames, + usernames, + sessionStatuses, regions, nodes, activeFilters, @@ -381,6 +391,8 @@ export class Filter extends React.Component { showRegions, showNodes, timeLabel, + showUsername, + showSessionStatus, } = this.props; const dropdownArea = hide ? hidden : dropdown; const customStyles = { @@ -460,6 +472,55 @@ export class Filter extends React.Component { ); + const usernameOptions = showUsername + ? usernames.map(username => ({ + label: username, + value: username, + isSelected: this.isOptionSelected(username, filters.username), + })) + : []; + const usernameValue = usernameOptions.filter(option => { + return filters.username.split(",").includes(option.label); + }); + const usernameFilter = ( +
+
Username
+ +
+ ); + + const sessionStatusOptions = showSessionStatus + ? sessionStatuses.map(sessionStatus => ({ + label: sessionStatus, + value: sessionStatus, + isSelected: this.isOptionSelected( + sessionStatus, + filters.sessionStatus, + ), + })) + : []; + const sessionStatusValue = sessionStatusOptions.filter(option => { + return filters.sessionStatus.split(",").includes(option.label); + }); + const sessionStatusFilter = ( +
+
Session Status
+ +
+ ); + const regionsOptions = showRegions ? regions.map(region => ({ label: region, @@ -573,6 +634,8 @@ export class Filter extends React.Component {
{appFilter} {showDB ? dbFilter : ""} + {showUsername ? usernameFilter : ""} + {showSessionStatus ? sessionStatusFilter : ""} {showSqlType ? sqlTypeFilter : ""} {showRegions ? regionsFilter : ""} {showNodes ? nodesFilter : ""} diff --git a/pkg/ui/workspaces/cluster-ui/src/sessions/sessionDetails.module.scss b/pkg/ui/workspaces/cluster-ui/src/sessions/sessionDetails.module.scss index 5b30e6d9b5ab..80d2525041c3 100644 --- a/pkg/ui/workspaces/cluster-ui/src/sessions/sessionDetails.module.scss +++ b/pkg/ui/workspaces/cluster-ui/src/sessions/sessionDetails.module.scss @@ -68,4 +68,10 @@ width: 20px; fill: $colors--functional-orange-3; } + + &__closed { + height: 10px; + width: 20px; + fill: $colors--neutral-4; + } } diff --git a/pkg/ui/workspaces/cluster-ui/src/sessions/sessionDetails.tsx b/pkg/ui/workspaces/cluster-ui/src/sessions/sessionDetails.tsx index fd6ce77328df..efbda89016d0 100644 --- a/pkg/ui/workspaces/cluster-ui/src/sessions/sessionDetails.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/sessions/sessionDetails.tsx @@ -16,7 +16,11 @@ import { Loading } from "../loading"; import _ from "lodash"; import { Link, RouteComponentProps } from "react-router-dom"; -import { SessionInfo } from "./sessionsTable"; +import { + getStatusClassname, + getStatusString, + SessionInfo, +} from "./sessionsTable"; import { SummaryCard, SummaryCardItem } from "../summaryCard"; import SQLActivityError from "../sqlActivity/errorComponent"; @@ -394,15 +398,9 @@ export class SessionDetails extends React.Component { value={
0 - ? "session-status-icon__active" - : "session-status-icon__idle", - )} + className={cx(getStatusClassname(session.status))} /> - - {session.active_queries.length > 0 ? "Active" : "Idle"} - + {getStatusString(session.status)}
} className={cx("details-item")} diff --git a/pkg/ui/workspaces/cluster-ui/src/sessions/sessionDetailsPage.fixture.ts b/pkg/ui/workspaces/cluster-ui/src/sessions/sessionDetailsPage.fixture.ts index 55b7dd57dcb0..53dce50c2d1c 100644 --- a/pkg/ui/workspaces/cluster-ui/src/sessions/sessionDetailsPage.fixture.ts +++ b/pkg/ui/workspaces/cluster-ui/src/sessions/sessionDetailsPage.fixture.ts @@ -12,6 +12,7 @@ import { createMemoryHistory } from "history"; import { SessionDetailsProps } from "./sessionDetails"; import { activeSession, + closedSession, idleSession, idleTransactionSession, } from "./sessionsPage.fixture"; @@ -69,6 +70,11 @@ export const sessionDetailsActiveStmtPropsFixture: SessionDetailsProps = { session: activeSession, }; +export const sessionDetailsClosedPropsFixture: SessionDetailsProps = { + ...sessionDetailsPropsBase, + session: closedSession, +}; + export const sessionDetailsNotFound: SessionDetailsProps = { ...sessionDetailsPropsBase, session: { session: null }, diff --git a/pkg/ui/workspaces/cluster-ui/src/sessions/sessionDetailsPage.stories.tsx b/pkg/ui/workspaces/cluster-ui/src/sessions/sessionDetailsPage.stories.tsx index a87bc45f4dfb..0bc52feca669 100644 --- a/pkg/ui/workspaces/cluster-ui/src/sessions/sessionDetailsPage.stories.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/sessions/sessionDetailsPage.stories.tsx @@ -16,6 +16,7 @@ import { SessionDetails } from "./sessionDetails"; import { sessionDetailsActiveStmtPropsFixture, sessionDetailsActiveTxnPropsFixture, + sessionDetailsClosedPropsFixture, sessionDetailsIdlePropsFixture, sessionDetailsNotFound, } from "./sessionDetailsPage.fixture"; @@ -32,6 +33,9 @@ storiesOf("Session Details Page", module) .add("Session", () => ( )) + .add("Closed Session", () => ( + + )) .add("Session Not Found", () => ( )); diff --git a/pkg/ui/workspaces/cluster-ui/src/sessions/sessionsPage.fixture.ts b/pkg/ui/workspaces/cluster-ui/src/sessions/sessionsPage.fixture.ts index 05140c7c720f..9ab8b2746f8e 100644 --- a/pkg/ui/workspaces/cluster-ui/src/sessions/sessionsPage.fixture.ts +++ b/pkg/ui/workspaces/cluster-ui/src/sessions/sessionsPage.fixture.ts @@ -47,7 +47,7 @@ export const idleSession: SessionInfo = { alloc_bytes: Long.fromNumber(0), max_alloc_bytes: Long.fromNumber(10240), active_queries: [], - status: Status.ACTIVE, + status: Status.IDLE, toJSON: () => ({}), }, }; @@ -85,7 +85,7 @@ export const idleTransactionSession: SessionInfo = { }, last_active_query_no_constants: "SHOW database", active_queries: [], - status: Status.ACTIVE, + status: Status.IDLE, toJSON: () => ({}), }, }; @@ -142,10 +142,36 @@ export const activeSession: SessionInfo = { }, }; +export const closedSession: SessionInfo = { + session: { + node_id: 1, + username: "root", + client_address: "127.0.0.1:57618", + application_name: "$ cockroach sql", + start: { + seconds: Long.fromNumber(1596816670), + nanos: 369989000, + }, + last_active_query: "SHOW database", + id: toUuid("FekiTsjUZoAAAAAAAAAAAQ=="), + last_active_query_no_constants: "SHOW database", + alloc_bytes: Long.fromNumber(0), + max_alloc_bytes: Long.fromNumber(10240), + active_queries: [], + end: { + seconds: Long.fromNumber(1596819870), + nanos: 369989000, + }, + status: Status.CLOSED, + toJSON: () => ({}), + }, +}; + const sessionsList: SessionInfo[] = [ idleSession, idleTransactionSession, activeSession, + closedSession, ]; export const filters: Filters = { diff --git a/pkg/ui/workspaces/cluster-ui/src/sessions/sessionsPage.tsx b/pkg/ui/workspaces/cluster-ui/src/sessions/sessionsPage.tsx index 2c5dd1b56730..8b3c648aa5b7 100644 --- a/pkg/ui/workspaces/cluster-ui/src/sessions/sessionsPage.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/sessions/sessionsPage.tsx @@ -13,6 +13,7 @@ import { isNil, merge } from "lodash"; import { syncHistory } from "src/util/query"; import { + getStatusString, makeSessionsColumns, SessionInfo, SessionsSortedTable, @@ -112,6 +113,20 @@ function getSessionAppFilterOptions(sessions: SessionInfo[]): string[] { return Array.from(uniqueAppNames).sort(); } +function getSessionUsernameFilterOptions(sessions: SessionInfo[]): string[] { + const uniqueUsernames = new Set(sessions.map(s => s.session.username)); + + return Array.from(uniqueUsernames).sort(); +} + +function getSessionStatusFilterOptions(sessions: SessionInfo[]): string[] { + const uniqueStatuses = new Set( + sessions.map(s => getStatusString(s.session.status)), + ); + + return Array.from(uniqueStatuses).sort(); +} + export class SessionsPage extends React.Component< SessionsPageProps, SessionsPageState @@ -298,6 +313,20 @@ export class SessionsPage extends React.Component< "seconds", ); return sessionTime >= timeValue || timeValue === "empty"; + }) + .filter((s: SessionInfo) => { + if (filters.username && filters.username != "All") { + const usernames = filters.username.split(","); + return usernames.includes(s.session.username); + } + return true; + }) + .filter((s: SessionInfo) => { + if (filters.sessionStatus && filters.sessionStatus != "All") { + const statuses = filters.sessionStatus.split(","); + return statuses.includes(getStatusString(s.session.status)); + } + return true; }); return { @@ -317,6 +346,8 @@ export class SessionsPage extends React.Component< } = this.getFilteredSessionsData(); const appNames = getSessionAppFilterOptions(sessionsData); + const usernames = getSessionUsernameFilterOptions(sessionsData); + const sessionStatuses = getSessionStatusFilterOptions(sessionsData); const columns = makeSessionsColumns( "session", this.terminateSessionRef, @@ -354,6 +385,10 @@ export class SessionsPage extends React.Component< { const { session } = props; - const status = session.active_queries.length > 0 ? "Active" : "Idle"; - const classname = - session.active_queries.length > 0 - ? "session-status-icon__active" - : "session-status-icon__idle"; + const status = getStatusString(session.status); + const classname = getStatusClassname(session.status); return (