diff --git a/pkg/ui/workspaces/cluster-ui/src/api/index.ts b/pkg/ui/workspaces/cluster-ui/src/api/index.ts index 346d6d395a59..c6c985b1e5e7 100644 --- a/pkg/ui/workspaces/cluster-ui/src/api/index.ts +++ b/pkg/ui/workspaces/cluster-ui/src/api/index.ts @@ -18,3 +18,4 @@ export * from "./insightsApi"; export * from "./indexActionsApi"; export * from "./schemaInsightsApi"; export * from "./schedulesApi"; +export * from "./tracezApi"; diff --git a/pkg/ui/workspaces/cluster-ui/src/api/tracezApi.ts b/pkg/ui/workspaces/cluster-ui/src/api/tracezApi.ts new file mode 100644 index 000000000000..42de802fbe93 --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/api/tracezApi.ts @@ -0,0 +1,81 @@ +// Copyright 2022 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 { cockroach } from "@cockroachlabs/crdb-protobuf-client"; +import { fetchData } from "src/api"; +import TakeTracingSnapshotRequest = cockroach.server.serverpb.TakeTracingSnapshotRequest; + +export type ListTracingSnapshotsRequestMessage = + cockroach.server.serverpb.ListTracingSnapshotsRequest; +export type ListTracingSnapshotsResponseMessage = + cockroach.server.serverpb.ListTracingSnapshotsResponse; + +export type TakeTracingSnapshotRequestMessage = TakeTracingSnapshotRequest; +export type TakeTracingSnapshotResponseMessage = + cockroach.server.serverpb.TakeTracingSnapshotResponse; + +export type GetTracingSnapshotRequestMessage = + cockroach.server.serverpb.GetTracingSnapshotRequest; +export type GetTracingSnapshotResponseMessage = + cockroach.server.serverpb.GetTracingSnapshotResponse; + +export type Span = cockroach.server.serverpb.ITracingSpan; +export type Snapshot = cockroach.server.serverpb.ITracingSnapshot; + +export type GetTraceRequestMessage = cockroach.server.serverpb.GetTraceRequest; +export type GetTraceResponseMessage = + cockroach.server.serverpb.GetTraceResponse; + +const API_PREFIX = "_admin/v1"; + +export function listTracingSnapshots(): Promise { + return fetchData( + cockroach.server.serverpb.ListTracingSnapshotsResponse, + `${API_PREFIX}/trace_snapshots`, + null, + null, + "30M", // 30 minute timeout. + ); +} + +export function takeTracingSnapshot(): Promise { + const req = new TakeTracingSnapshotRequest(); + return fetchData( + cockroach.server.serverpb.TakeTracingSnapshotResponse, + `${API_PREFIX}/trace_snapshots`, + req as any, + null, + "30M", + ); +} + +export function getTracingSnapshot( + snapshotID: number, +): Promise { + return fetchData( + cockroach.server.serverpb.GetTracingSnapshotResponse, + `${API_PREFIX}/trace_snapshots/${snapshotID}`, + null, + null, + "30M", + ); +} + +export function getTraceForSnapshot( + req: GetTraceRequestMessage, +): Promise { + return fetchData( + cockroach.server.serverpb.GetTraceResponse, + `${API_PREFIX}/traces`, + req as any, + null, + "30M", + ); +} diff --git a/pkg/ui/workspaces/cluster-ui/src/index.ts b/pkg/ui/workspaces/cluster-ui/src/index.ts index 971f59f77c68..a085f589b30b 100644 --- a/pkg/ui/workspaces/cluster-ui/src/index.ts +++ b/pkg/ui/workspaces/cluster-ui/src/index.ts @@ -46,6 +46,7 @@ export * from "./store"; export * from "./transactionsPage"; export * from "./transactionDetails"; export * from "./text"; +export * from "./tracez"; export { util, api }; export * from "./sessions"; export * from "./timeScaleDropdown"; diff --git a/pkg/ui/workspaces/cluster-ui/src/tracez/index.ts b/pkg/ui/workspaces/cluster-ui/src/tracez/index.ts new file mode 100644 index 000000000000..01a322a38a5a --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/tracez/index.ts @@ -0,0 +1,11 @@ +// Copyright 2022 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. + +export * from "./snapshot"; diff --git a/pkg/ui/workspaces/cluster-ui/src/tracez/snapshot.module.scss b/pkg/ui/workspaces/cluster-ui/src/tracez/snapshot.module.scss new file mode 100644 index 000000000000..c0609f2b5b83 --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/tracez/snapshot.module.scss @@ -0,0 +1,70 @@ +// Copyright 2022 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 "src/core/index.module.scss"; + +.snapshots-page { + display: flex; + flex-flow: column; + height: 100%; +} + +.no-results { + display: flex; + align-items: center; + justify-content: center; + height: 100%; +} + +.table-row { + vertical-align: top; + height: 4px; // Override the default row-wrapper height of 70px. +} + +.table-cell { + padding: 4px 0px 4px 16px; // For vertical, override default of 16px. +} + +.operation-cell { + padding: 4px 0px 4px 0px; // For vertical, override default of 16px. +} + +.icon-cell { + padding: 4px 4px 4px 0px; + margin: 0px; + display: flex; + justify-content: right; +} + +.table-cell-duration { + padding: 4px 16px; // For vertical, override default of 16px. + display: flex; + justify-content: right; + min-width: 22ch; +} + +.tag-group-cell { + display: flex; + flex-direction: row; +} + +.tag-cell { + gap: 4px; + display: flex; + flex-direction: column; +} + +.plus-minus { + border-style: solid; + border-width: 1px; + height: 12px; + width: 12px; + margin-top: 4px; +} diff --git a/pkg/ui/workspaces/cluster-ui/src/tracez/snapshot/index.ts b/pkg/ui/workspaces/cluster-ui/src/tracez/snapshot/index.ts new file mode 100644 index 000000000000..ac6684c142de --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/tracez/snapshot/index.ts @@ -0,0 +1,12 @@ +// Copyright 2022 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. + +export * from "./snapshotPage"; +export * from "./spanTable"; diff --git a/pkg/ui/workspaces/cluster-ui/src/tracez/snapshot/snapshotPage.spec.tsx b/pkg/ui/workspaces/cluster-ui/src/tracez/snapshot/snapshotPage.spec.tsx new file mode 100644 index 000000000000..7970bbe874fc --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/tracez/snapshot/snapshotPage.spec.tsx @@ -0,0 +1,86 @@ +// Copyright 2022 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 { SnapshotPage, SnapshotPageProps } from "./snapshotPage"; +import { render } from "@testing-library/react"; +import React from "react"; +import { MemoryRouter } from "react-router-dom"; +import * as H from "history"; + +import { SortSetting } from "../../sortedtable"; +import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; +import GetTracingSnapshotResponse = cockroach.server.serverpb.GetTracingSnapshotResponse; + +const getMockSnapshotPageProps = (): SnapshotPageProps => { + const history = H.createHashHistory(); + return { + location: history.location, + history, + match: { + url: "", + path: history.location.pathname, + isExact: false, + params: {}, + }, + refreshSnapshot: (id: number): void => {}, + refreshSnapshots: (): void => {}, + setSort: (value: SortSetting): void => {}, + snapshotError: undefined, + snapshotLoading: false, + snapshots: undefined, + snapshotsError: undefined, + snapshotsLoading: false, + sort: undefined, + snapshot: null, + }; +}; + +describe("Snapshot", () => { + it("renders expected snapshot table columns", () => { + const props = getMockSnapshotPageProps(); + props.snapshot = GetTracingSnapshotResponse.fromObject({ + snapshot: { + spans: [{ span_id: 1 }], + }, + }); + const { getByText } = render( + + + , + ); + const expectedColumnTitles = [ + "Span", + "Start Time (UTC)", + "Duration", + "Expandable Tags", + "Tags", + ]; + + for (const columnTitle of expectedColumnTitles) { + getByText(columnTitle); + } + }); + + it("renders a message when the table is empty", () => { + const { getByText } = render( + + + , + ); + const expectedText = [ + "No spans to show", + "Spans provide debug information.", + ]; + + for (const text of expectedText) { + getByText(text); + } + }); +}); diff --git a/pkg/ui/workspaces/cluster-ui/src/tracez/snapshot/snapshotPage.tsx b/pkg/ui/workspaces/cluster-ui/src/tracez/snapshot/snapshotPage.tsx new file mode 100644 index 000000000000..300020da70b8 --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/tracez/snapshot/snapshotPage.tsx @@ -0,0 +1,169 @@ +// Copyright 2022 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 { InlineAlert } from "@cockroachlabs/ui-components"; +import moment from "moment"; +import React, { useEffect } from "react"; +import { Helmet } from "react-helmet"; +import { RouteComponentProps } from "react-router-dom"; +import { generatePath } from "react-router"; +import { + ListTracingSnapshotsResponseMessage, + GetTracingSnapshotResponseMessage, +} from "src/api/tracezApi"; +import { Delayed } from "src/delayed"; +import { Dropdown, DropdownOption } from "src/dropdown"; +import { Loading } from "src/loading"; +import { PageConfig, PageConfigItem } from "src/pageConfig"; +import { SortSetting } from "src/sortedtable"; +import { getMatchParamByName, TimestampToMoment } from "src/util"; +import { syncHistory } from "src/util"; +import { Location, History } from "history"; + +import { SpanTable } from "./spanTable"; + +import { commonStyles } from "src/common"; +import styles from "../snapshot.module.scss"; +import classNames from "classnames/bind"; + +const cx = classNames.bind(styles); + +export interface SnapshotPageStateProps { + sort: SortSetting; + snapshotsError: Error | null; + snapshotsLoading: boolean; + snapshots: ListTracingSnapshotsResponseMessage; + snapshot: GetTracingSnapshotResponseMessage; + snapshotError: Error | null; + snapshotLoading: boolean; +} + +export interface SnapshotPageDispatchProps { + setSort: (value: SortSetting) => void; + refreshSnapshots: () => void; + refreshSnapshot: (id: number) => void; +} + +export type SnapshotPageProps = SnapshotPageStateProps & + SnapshotPageDispatchProps & + RouteComponentProps; + +export const SnapshotPage: React.FC = props => { + const { + history, + match, + refreshSnapshots, + refreshSnapshot, + snapshots, + snapshot, + sort, + setSort, + } = props; + + const searchParams = new URLSearchParams(history.location.search); + + // Sort Settings. + const ascending = (searchParams.get("ascending") || undefined) === "true"; + const columnTitle = searchParams.get("columnTitle") || ""; + + const snapshotIDStr = getMatchParamByName(match, "snapshotID"); + + useEffect(() => { + refreshSnapshots(); + }); + + useEffect(() => { + if (snapshotIDStr || !snapshots) { + return; + } + const snapArray = snapshots.snapshots; + const lastSnapshotID = snapArray[snapArray.length - 1].snapshot_id; + history.location.pathname = "/debug/tracez_v2/snapshot/" + lastSnapshotID; + history.replace(history.location); + }, [snapshots, snapshotIDStr, history]); + + useEffect(() => { + if (!columnTitle) { + return; + } + setSort({ columnTitle, ascending }); + }, [setSort, columnTitle, ascending]); + + useEffect(() => { + if (!snapshotIDStr) { + return; + } + refreshSnapshot(parseInt(snapshotIDStr)); + }, [snapshotIDStr, refreshSnapshot]); + + const onSnapshotSelected = (item: string) => { + history.location.pathname = "/debug/tracez_v2/snapshot/" + item; + history.push(history.location); + }; + + const changeSortSetting = (ss: SortSetting): void => { + setSort(ss); + syncHistory( + { + ascending: ss.ascending.toString(), + columnTitle: ss.columnTitle, + }, + history, + ); + }; + + const snapshotItems = !snapshots + ? [] + : snapshots.snapshots.map(snapshotInfo => { + const id = snapshotInfo.snapshot_id.toString(); + const time = TimestampToMoment(snapshotInfo.captured_at).format( + "MMM D, YYYY [at] HH:mm:ss", + ); + return { + name: "Snapshot " + id + ": " + time, + value: id, + }; + }); + + const isLoading = props.snapshotsLoading || props.snapshotLoading; + const error = props.snapshotsError || props.snapshotError; + return ( +
+ +

Snapshots

+
+ + + + {snapshotIDStr && + snapshotItems.length && + snapshotItems.find(option => option["value"] === snapshotIDStr)[ + "name" + ]} + + + +
+
+ ( + + )} + /> +
+
+ ); +}; diff --git a/pkg/ui/workspaces/cluster-ui/src/tracez/snapshot/spanTable.tsx b/pkg/ui/workspaces/cluster-ui/src/tracez/snapshot/spanTable.tsx new file mode 100644 index 000000000000..96d7eeddf723 --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/tracez/snapshot/spanTable.tsx @@ -0,0 +1,248 @@ +// Copyright 2022 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 moment from "moment"; +import { max } from "lodash"; +import React, { useState } from "react"; +import { Nodes, Caution, Plus, Minus, Eye, Pencil } from "@cockroachlabs/icons"; +import { Span, Snapshot } from "src/api/tracezApi"; +import { EmptyTable } from "src/empty"; +import { ColumnDescriptor, SortSetting, SortedTable } from "src/sortedtable"; + +import styles from "../snapshot.module.scss"; +import classNames from "classnames/bind"; +import { TimestampToMoment } from "src/util"; +import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; +import ISpanTag = cockroach.server.serverpb.ISpanTag; +import RecordingMode = cockroach.util.tracing.tracingpb.RecordingMode; +const cx = classNames.bind(styles); + +class SpanSortedTable extends SortedTable {} + +interface TagRowProps { + t: ISpanTag; +} +const TagRow = ({ t }: TagRowProps) => { + let highlight = null; + if (t.highlight) { + highlight = ; + } + let arrow = null; + if (t.inherited) { + arrow = " ↓"; + } else if (t.copied_from_child) { + arrow = " ↑"; + } + return ( + + + {highlight} + {t.key} + {arrow} + {t.val && ":"} +   + + {t.val} + + ); +}; + +const TagCell = (props: { span: Span }) => { + const tags = props.span.processed_tags.filter(tag => !tag?.children?.length); + const maxChars = max(tags.map(tag => tag.key.length + tag.val.length)); + return ( + + + {tags.map((t, i) => ( + + ))} + +
+ ); +}; + +const TagGroup = (props: { tag: ISpanTag }) => { + const { tag } = props; + const [isExpanded, setExpanded] = useState(false); + const { children } = tag; + const maxChars = max( + children.map(childTag => childTag.key.length + childTag.val.length), + ); + return ( +
+ {!isExpanded && ( +
+ { + setExpanded(!isExpanded); + }} + /> +
+   + {tag.key + " (" + tag.children.length + ")"} +
+
+ )} + {isExpanded && ( +
+ { + setExpanded(!isExpanded); + }} + /> +
+   + {tag.key + " (" + tag.children.length + ")"} + + + {children.map((child, i) => ( + + + + + ))} + +
+ {child.key + ":"}  + {child.val}
+
+
+ )} +
+ ); +}; + +const TagGroupCell = (props: { span: Span }) => { + const tagGroups = props.span.processed_tags.filter( + tag => tag?.children?.length, + ); + + return ( +
+ {tagGroups.map((t, i) => ( + + ))} +
+ ); +}; + +const formatDuration = (d: moment.Duration): string => { + const hours = Math.floor(d.asHours()); + const hourStr = hours ? hours + "h " : ""; + const minutes = d.minutes(); + let minuteStr = ""; + if (hourStr) { + minuteStr = minutes.toFixed(0).padStart(2, "0") + "m "; + } else if (minutes) { + minuteStr = minutes + "m "; + } + const secondPadNum = hourStr || minuteStr ? 2 : 1; + const secondStr = d.seconds().toFixed(0).padStart(secondPadNum, "0"); + const msStr = "." + d.milliseconds().toFixed(0).padEnd(3, "0") + "s"; + return hourStr + minuteStr + secondStr + msStr; +}; +const makeColumns = (snapshot: Snapshot): ColumnDescriptor[] => { + const startTime = TimestampToMoment(snapshot.captured_at); + return [ + { + name: "icons", + title: "", + cell: span => { + return ( + <> + {span.current && } + {span.current_recording_mode != RecordingMode.OFF && } + + ); + }, + className: cx("icon-cell"), + }, + { + name: "span", + title: "Span", + titleAlign: "left", + cell: span => span.operation, + sort: span => span.operation, + hideTitleUnderline: true, + className: cx("operation-cell"), + }, + { + name: "start time", + title: "Start Time (UTC)", + titleAlign: "right", + cell: span => TimestampToMoment(span.start).format("YYYY-MM-DD HH:mm:ss"), + sort: span => TimestampToMoment(span.start), + hideTitleUnderline: true, + className: cx("table-cell"), + }, + { + name: "duration", + title: "Duration", + titleAlign: "right", + cell: span => { + return formatDuration( + moment.duration(startTime.diff(TimestampToMoment(span.start))), + ); + }, + sort: span => startTime.diff(TimestampToMoment(span.start)), + hideTitleUnderline: true, + className: cx("table-cell-duration"), + }, + { + name: "tagGroups", + title: "Expandable Tags", + titleAlign: "left", + cell: span => , + hideTitleUnderline: true, + className: cx("table-cell"), + }, + { + name: "tags", + title: "Tags", + titleAlign: "left", + cell: span => , + hideTitleUnderline: true, + className: cx("table-cell"), + }, + ]; +}; + +export interface SpanTableProps { + sort: SortSetting; + setSort: (value: SortSetting) => void; + snapshot: Snapshot; +} + +export const SpanTable: React.FC = props => { + const { snapshot, sort, setSort } = props; + const spans = snapshot?.spans; + + if (!spans) { + return ( + } + message="Spans provide debug information." + /> + ); + } + + return ( + cx("table-row")} + /> + ); +}; diff --git a/pkg/ui/workspaces/db-console/src/app.spec.tsx b/pkg/ui/workspaces/db-console/src/app.spec.tsx index 8a9793d8abce..bd013854bb05 100644 --- a/pkg/ui/workspaces/db-console/src/app.spec.tsx +++ b/pkg/ui/workspaces/db-console/src/app.spec.tsx @@ -49,6 +49,7 @@ stubComponentInModule( ); stubComponentInModule("src/views/schedules/schedulesPage", "default"); stubComponentInModule("src/views/schedules/scheduleDetails", "default"); +stubComponentInModule("src/views/tracez_v2/snapshotPage", "default"); import React from "react"; import { Action, Store } from "redux"; @@ -503,6 +504,13 @@ describe("Routing to", () => { }); }); + describe("'/debug/tracez_v2/snapshot/:id' path", () => { + test("routes to component", () => { + navigateToPath("/debug/tracez_v2/snapshot/12345"); + screen.getByTestId("snapshotPage"); + }); + }); + { /* raft pages */ } diff --git a/pkg/ui/workspaces/db-console/src/app.tsx b/pkg/ui/workspaces/db-console/src/app.tsx index 3cd8310128fb..b6d04817f046 100644 --- a/pkg/ui/workspaces/db-console/src/app.tsx +++ b/pkg/ui/workspaces/db-console/src/app.tsx @@ -80,6 +80,7 @@ import ActiveStatementDetails from "./views/statements/activeStatementDetailsCon import ActiveTransactionDetails from "./views/transactions/activeTransactionDetailsConnected"; import "styl/app.styl"; import { Tracez } from "src/views/tracez/tracez"; +import SnapshotPage from "src/views/tracez_v2/snapshotPage"; import InsightsOverviewPage from "src/views/insights/insightsOverview"; import TransactionInsightDetailsPageConnected from "src/views/insights/transactionInsightDetailsPageConnected"; import StatementInsightDetailsPageConnected from "src/views/insights/statementInsightDetailsPageConnected"; @@ -322,6 +323,16 @@ export const App: React.FC = (props: AppProps) => { {/* debug pages */} + + "list"; + +const snapshotsReducerObj = new KeyedCachedDataReducer( + clusterUiApi.listTracingSnapshots, + "snapshots", + snapshotsKey, + moment.duration(10, "s"), + moment.duration(1, "minute"), +); +export const refreshSnapshots = snapshotsReducerObj.refresh; + +export const snapshotKey = (snapshotID: number): string => + snapshotID.toString(); + +const snapshotReducerObj = new KeyedCachedDataReducer( + clusterUiApi.getTracingSnapshot, + "snapshot", + snapshotKey, + moment.duration(10, "s"), +); +export const refreshSnapshot = snapshotReducerObj.refresh; + export interface APIReducersState { cluster: CachedDataReducerState; events: CachedDataReducerState; @@ -496,6 +518,8 @@ export interface APIReducersState { schemaInsights: CachedDataReducerState; schedules: KeyedCachedDataReducerState; schedule: KeyedCachedDataReducerState; + snapshots: KeyedCachedDataReducerState; + snapshot: KeyedCachedDataReducerState; } export const apiReducersReducer = combineReducers({ @@ -546,6 +570,8 @@ export const apiReducersReducer = combineReducers({ [schemaInsightsReducerObj.actionNamespace]: schemaInsightsReducerObj.reducer, [schedulesReducerObj.actionNamespace]: schedulesReducerObj.reducer, [scheduleReducerObj.actionNamespace]: scheduleReducerObj.reducer, + [snapshotsReducerObj.actionNamespace]: snapshotsReducerObj.reducer, + [snapshotReducerObj.actionNamespace]: snapshotReducerObj.reducer, }); export { CachedDataReducerState, KeyedCachedDataReducerState }; diff --git a/pkg/ui/workspaces/db-console/src/views/tracez_v2/snapshotPage.tsx b/pkg/ui/workspaces/db-console/src/views/tracez_v2/snapshotPage.tsx new file mode 100644 index 000000000000..d48079e079c9 --- /dev/null +++ b/pkg/ui/workspaces/db-console/src/views/tracez_v2/snapshotPage.tsx @@ -0,0 +1,105 @@ +// Copyright 2022 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 { + api, + SnapshotPage, + SnapshotPageStateProps, + SortSetting, +} from "@cockroachlabs/cluster-ui"; +import { connect } from "react-redux"; +import { RouteComponentProps, withRouter } from "react-router-dom"; +import { createSelector } from "reselect"; +import { + CachedDataReducerState, + snapshotsKey, + refreshSnapshot, + refreshSnapshots, +} from "src/redux/apiReducers"; +import { LocalSetting } from "src/redux/localsettings"; +import { AdminUIState } from "src/redux/state"; +import { getMatchParamByName } from "src/util/query"; + +export const sortSetting = new LocalSetting( + "sortSetting/spans", + s => s.localSettings, + { columnTitle: "creationTime", ascending: false }, +); + +const selectSnapshotsState = createSelector( + [ + (state: AdminUIState) => state.cachedData.snapshots, + (_state: AdminUIState, key: string) => key, + ], + ( + snapshots, + key, + ): CachedDataReducerState => { + if (!snapshots) { + return null; + } + return snapshots[key]; + }, +); + +const selectSnapshotState = createSelector( + [ + (state: AdminUIState) => state.cachedData.snapshot, + (_state: AdminUIState, props: RouteComponentProps) => props, + ], + ( + snapshot, + props, + ): CachedDataReducerState => { + const snapshotID = getMatchParamByName(props.match, "snapshotID"); + if (!snapshot) { + return null; + } + return snapshot[snapshotID]; + }, +); + +const mapStateToProps = ( + state: AdminUIState, + props: RouteComponentProps, +): SnapshotPageStateProps => { + const sort = sortSetting.selector(state); + + const snapshotsState = selectSnapshotsState(state, snapshotsKey()); + const snapshots = snapshotsState ? snapshotsState.data : null; + const snapshotsLoading = snapshotsState ? snapshotsState.inFlight : false; + const snapshotsError = snapshotsState ? snapshotsState.lastError : null; + + const snapshotState = selectSnapshotState(state, props); + const snapshot = snapshotState ? snapshotState.data : null; + const snapshotLoading = snapshotState ? snapshotState.inFlight : false; + const snapshotError = snapshotState ? snapshotState.lastError : null; + + return { + sort, + + snapshots, + snapshotsLoading, + snapshotsError, + + snapshot, + snapshotLoading, + snapshotError, + }; +}; + +const mapDispatchToProps = { + setSort: sortSetting.set, + refreshSnapshots, + refreshSnapshot, +}; + +export default withRouter( + connect(mapStateToProps, mapDispatchToProps)(SnapshotPage), +);