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 (