Skip to content

Commit

Permalink
feat: create tag-explorer page for analyzing tag breakdowns (grafana#…
Browse files Browse the repository at this point in the history
…1293)

* add explore route
* add explore page to sidebar
* add explore page basic structure
  • Loading branch information
dogfrogfog authored Jul 29, 2022
1 parent 597b20a commit 5456a86
Show file tree
Hide file tree
Showing 20 changed files with 882 additions and 49 deletions.
14 changes: 14 additions & 0 deletions packages/pyroscope-models/src/groups.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { z } from 'zod';

const GroupSchema = z.object({
watermark: z.object({}).optional(),
// timeline data
startTime: z.number(),
samples: z.array(z.number()),
durationDelta: z.number(),
});

export const GroupsSchema = z.record(z.string(), GroupSchema);

export type Groups = z.infer<typeof GroupsSchema>;
export type Group = z.infer<typeof GroupSchema>;
1 change: 1 addition & 0 deletions packages/pyroscope-models/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './profile';
export * from './flamebearer';
export * from './trace';
export * from './groups';
export * from './decode';
export * from './spyName';
3 changes: 2 additions & 1 deletion pkg/server/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,8 @@ func (ctrl *Controller) serverMux() (http.Handler, error) {
{"/settings", ih},
{"/settings/{page}", ih},
{"/settings/{page}/{subpage}", ih},
{"/forbidden", ih}},
{"/forbidden", ih},
{"/explore", ih}},
ctrl.drainMiddleware,
ctrl.authMiddleware(ctrl.indexHandler()))

Expand Down
11 changes: 11 additions & 0 deletions webapp/javascript/components/AppSelector/AppSelector.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,17 @@ describe('AppSelector', () => {
type: 'loaded',
data: mockAppNames,
},
tagExplorerView: {
groupByTag: '',
groupByTagValue: '',
type: 'pristine',
groups: {},
timeline: {
startTime: 0,
samples: [],
durationDelta: 0,
},
},
},
},
});
Expand Down
13 changes: 12 additions & 1 deletion webapp/javascript/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { faChevronLeft } from '@fortawesome/free-solid-svg-icons/faChevronLeft';
import { faSignOutAlt } from '@fortawesome/free-solid-svg-icons/faSignOutAlt';
import { faHandPointRight } from '@fortawesome/free-solid-svg-icons/faHandPointRight';
import { faSync } from '@fortawesome/free-solid-svg-icons/faSync';
import { faSearch } from '@fortawesome/free-solid-svg-icons/faSearch';
import Sidebar, {
MenuItem,
SidebarHeader,
Expand Down Expand Up @@ -79,6 +80,7 @@ export function SidebarComponent() {
PAGES.ADHOC_SINGLE,
PAGES.ADHOC_COMPARISON,
PAGES.ADHOC_COMPARISON_DIFF,
PAGES.TAG_EXPLORER,
] as string[]
).includes(pathname) || pathname.startsWith(PAGES.SETTINGS),
[pathname]
Expand All @@ -93,7 +95,8 @@ export function SidebarComponent() {
const isContinuousActive =
isRouteActive(PAGES.CONTINOUS_SINGLE_VIEW) ||
isRouteActive(PAGES.COMPARISON_VIEW) ||
isRouteActive(PAGES.COMPARISON_DIFF_VIEW);
isRouteActive(PAGES.COMPARISON_DIFF_VIEW) ||
isRouteActive(PAGES.TAG_EXPLORER);
const isAdhocActive =
isRouteActive(PAGES.ADHOC_SINGLE) ||
isRouteActive(PAGES.ADHOC_COMPARISON) ||
Expand Down Expand Up @@ -173,6 +176,14 @@ export function SidebarComponent() {
Continuous Profiling
</SidebarHeader>
)}
<MenuItem
data-testid="sidebar-explore-page"
active={isRouteActive(PAGES.TAG_EXPLORER)}
icon={<Icon icon={faSearch} />}
>
Tag explorer
<NavLink to={{ pathname: PAGES.TAG_EXPLORER, search }} exact />
</MenuItem>
<MenuItem
data-testid="sidebar-continuous-single"
active={isRouteActive(PAGES.CONTINOUS_SINGLE_VIEW)}
Expand Down
17 changes: 1 addition & 16 deletions webapp/javascript/components/TagsBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import TextareaAutosize from 'react-textarea-autosize';
import { Prism } from '@webapp/util/prism';
import { Query, brandQuery } from '@webapp/models/query';
import Input from '@webapp/ui/Input';
import { appendLabelToQuery } from '@webapp/util/appendLabelToQuery';
import styles from './TagsBar.module.css';

interface TagsBarProps {
Expand Down Expand Up @@ -278,22 +279,6 @@ function LabelsSubmenu({
return <Dropdown label="Select Tag">{Items}</Dropdown>;
}

function appendLabelToQuery(query: string, label: string, labelValue: string) {
const case1Regexp = new RegExp(`${label}=.+?(\\}|,)`);
if (query.match(case1Regexp)) {
return query.replace(case1Regexp, `${label}="${labelValue}"$1`);
}
if (query.indexOf('{}') !== -1) {
return query.replace('}', `${label}="${labelValue}"}`);
}
if (query.indexOf('}') !== -1) {
return query.replace('}', `, ${label}="${labelValue}"}`);
}

console.warn('TODO: handle this case');
return query;
}

// Identifies whether a label is in a query or not
function isLabelInQuery(query: string, label: string, labelValue: string) {
return query.includes(`${label}="${labelValue}"`);
Expand Down
79 changes: 71 additions & 8 deletions webapp/javascript/components/TimelineChartWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,20 @@
/* eslint-disable react/no-did-update-set-state */
/* eslint-disable react/destructuring-assignment */
import React, { ReactNode } from 'react';
import { Timeline } from '@webapp/models/timeline';
import Color from 'color';
import type { Group } from '@pyroscope/models/src';
import type { Timeline } from '@webapp/models/timeline';
import { formatAsOBject } from '@webapp/util/formatDate';
import Legend from '@webapp/pages/tagExplorer/components/Legend';
import TimelineChart from './TimelineChart';
import styles from './TimelineChartWrapper.module.css';

export interface TimelineGroupData {
data: Group;
tagName: string;
color?: Color;
}

interface TimelineData {
data?: Timeline;
color?: string;
Expand All @@ -19,18 +27,32 @@ interface Marking {
color: Color;
}

type TimelineChartWrapperProps = {
type TimelineDataProps =
| {
/** timelineA refers to the first (and maybe unique) timeline */
timelineA: TimelineData;
/** timelineB refers to the second timeline, useful for comparison view */
timelineB?: TimelineData;
/** to manage strict data type */
mode: 'singles';
}
| {
/** timelineGroups refers to group of timelines, useful for explore view */
timelineGroups: TimelineGroupData[];
/** show or hide tags legend, useful forr disabling single timeline legend */
showTagsLegend: boolean;
/** to manage strict data type */
mode: 'multiple';
};

type TimelineChartWrapperProps = TimelineDataProps & {
/** the id attribute of the element float will use to apply to, it should be unique */
id: string;

['data-testid']?: string;
onSelect: (from: string, until: string) => void;
format: 'lines' | 'bars';

/** timelineA refers to the first (and maybe unique) timeline */
timelineA: TimelineData;
/** timelineB refers to the second timeline, useful for comparison view */
timelineB?: TimelineData;
height?: string;

/** refers to the highlighted selection */
Expand All @@ -51,6 +73,7 @@ class TimelineChartWrapper extends React.Component<
// eslint-disable-next-line react/static-property-placement
static defaultProps = {
format: 'bars',
mode: 'singles',
};

constructor(props: TimelineChartWrapperProps) {
Expand Down Expand Up @@ -174,7 +197,48 @@ class TimelineChartWrapper extends React.Component<

render = () => {
const { flotOptions } = this.state;

if (this.props.mode === 'multiple') {
const { timelineGroups, showTagsLegend, id, timezone } = this.props;

const customFlotOptions = {
...flotOptions,
xaxis: {
...flotOptions.xaxis,
autoscaleMargin: null,
timezone: timezone || 'browser',
},
};

const centeredTimelineGroups =
timelineGroups &&
timelineGroups.map(({ data, color }) => {
return {
data: centerTimelineData({ data }),
color,
};
});

return (
<>
<TimelineChart
onSelect={this.props.onSelect}
className={styles.wrapper}
// eslint-disable-next-line react/destructuring-assignment
data-testid={this.props['data-testid']}
id={id}
options={customFlotOptions}
data={centeredTimelineGroups}
width="100%"
height={this.props.height || '100px'}
/>
{showTagsLegend && <Legend groups={timelineGroups} />}
</>
);
}

const { id, timelineA, timezone, title } = this.props;

// TODO deep copy
let timelineB = this.props.timelineB
? JSON.parse(JSON.stringify(this.props.timelineB))
Expand All @@ -187,7 +251,7 @@ class TimelineChartWrapper extends React.Component<
// In case there are few chunks left, then we'd like to add some margins to
// both sides making it look more centers
autoscaleMargin:
timelineA.data && timelineA.data.samples.length > 3 ? null : 0.005,
timelineA?.data && timelineA.data.samples.length > 3 ? null : 0.005,
timezone: timezone || 'browser',
},
};
Expand Down Expand Up @@ -238,7 +302,6 @@ class TimelineChartWrapper extends React.Component<
id={id}
options={customFlotOptions}
data={data}
// data={d}
width="100%"
height={this.props.height || '100px'}
/>
Expand Down
8 changes: 8 additions & 0 deletions webapp/javascript/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import store, { persistor } from './redux/store';
import ContinuousSingleView from './pages/ContinuousSingleView';
import ContinuousComparisonView from './pages/ContinuousComparisonView';
import ContinuousDiffView from './pages/ContinuousDiffView';
import TagExplorerView from './pages/TagExplorerView';
import Continuous from './components/Continuous';
import Settings from './components/Settings';
import Sidebar from './components/Sidebar';
Expand Down Expand Up @@ -85,6 +86,13 @@ function App() {
<ServiceDiscoveryApp />
</Protected>
</Route>
<Route exact path={PAGES.TAG_EXPLORER}>
<Protected>
<Continuous>
<TagExplorerView />
</Continuous>
</Protected>
</Route>
{isAdhocUIEnabled && (
<Route path={PAGES.ADHOC_SINGLE}>
<Protected>
Expand Down
77 changes: 77 additions & 0 deletions webapp/javascript/pages/TagExplorerView.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
.tagExplorerView {
.header {
display: flex;
align-items: center;
margin: 0 10px 10px;

.notSelectedTagDropdown {
border-color: var(--ps-selected-app);
color: var(--ps-selected-app);
}
}

.tableDescription {
margin: 0 10px 10px;
}

.title {
display: inline-block;
font-weight: 500;
font-size: 18px;
margin-right: 5px;
}

.query {
display: flex;
align-items: center;

.selectName {
font-weight: 500;
color: var(--ps-right-click-info);
margin-right: 5px;
}
}

.tagExplorerTable {
width: 100%;

thead,
tbody tr:nth-child(2n) {
background-color: var(--ps-neutral-9);
}

tbody {
cursor: pointer;

td {
font-weight: initial;
text-align: center;
}

tr.activeTagRow {
background-color: var(--ps-green-highlight);
color: var(--ps-tooltip-text);
}

.tagName {
display: flex;
align-items: center;

.tagColor {
display: inline-block;
height: 10px;
width: 10px;
border-radius: 2px;
margin-right: 10px;
}
}
}

td,
th {
border: 1px solid var(--ps-ui-border);
padding: 4px 10px;
width: 10%;
}
}
}
Loading

0 comments on commit 5456a86

Please sign in to comment.