mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-10-30 14:49:14 +00:00
335 lines
9.4 KiB
TypeScript
335 lines
9.4 KiB
TypeScript
import type {RouteComponentProps} from 'react-router';
|
|
import {browserHistory} from 'react-router';
|
|
import styled from '@emotion/styled';
|
|
import isEqual from 'lodash/isEqual';
|
|
import pick from 'lodash/pick';
|
|
|
|
import Feature from 'sentry/components/acl/feature';
|
|
import {Alert} from 'sentry/components/alert';
|
|
import {Breadcrumbs} from 'sentry/components/breadcrumbs';
|
|
import {Button} from 'sentry/components/button';
|
|
import {CompactSelect} from 'sentry/components/compactSelect';
|
|
import DeprecatedAsyncComponent from 'sentry/components/deprecatedAsyncComponent';
|
|
import * as Layout from 'sentry/components/layouts/thirds';
|
|
import SearchBar from 'sentry/components/searchBar';
|
|
import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
|
|
import Switch from 'sentry/components/switchButton';
|
|
import {t} from 'sentry/locale';
|
|
import {space} from 'sentry/styles/space';
|
|
import type {Organization, SavedQuery, SelectValue} from 'sentry/types';
|
|
import {trackAnalytics} from 'sentry/utils/analytics';
|
|
import EventView from 'sentry/utils/discover/eventView';
|
|
import {getDiscoverLandingUrl} from 'sentry/utils/discover/urls';
|
|
import {decodeScalar} from 'sentry/utils/queryString';
|
|
import withOrganization from 'sentry/utils/withOrganization';
|
|
|
|
import QueryList from './queryList';
|
|
import {getPrebuiltQueries, setRenderPrebuilt, shouldRenderPrebuilt} from './utils';
|
|
|
|
const SORT_OPTIONS: SelectValue<string>[] = [
|
|
{label: t('My Queries'), value: 'myqueries'},
|
|
{label: t('Recently Edited'), value: '-dateUpdated'},
|
|
{label: t('Query Name (A-Z)'), value: 'name'},
|
|
{label: t('Date Created (Newest)'), value: '-dateCreated'},
|
|
{label: t('Date Created (Oldest)'), value: 'dateCreated'},
|
|
{label: t('Most Outdated'), value: 'dateUpdated'},
|
|
{label: t('Most Popular'), value: 'mostPopular'},
|
|
{label: t('Recently Viewed'), value: 'recentlyViewed'},
|
|
];
|
|
|
|
type Props = {
|
|
organization: Organization;
|
|
} & RouteComponentProps<{}, {}> &
|
|
DeprecatedAsyncComponent['props'];
|
|
|
|
type State = {
|
|
savedQueries: SavedQuery[] | null;
|
|
savedQueriesPageLinks: string;
|
|
} & DeprecatedAsyncComponent['state'];
|
|
|
|
class DiscoverLanding extends DeprecatedAsyncComponent<Props, State> {
|
|
state: State = {
|
|
// AsyncComponent state
|
|
loading: true,
|
|
reloading: false,
|
|
error: false,
|
|
errors: {},
|
|
|
|
// local component state
|
|
renderPrebuilt: shouldRenderPrebuilt(),
|
|
savedQueries: null,
|
|
savedQueriesPageLinks: '',
|
|
};
|
|
|
|
shouldReload = true;
|
|
|
|
getSavedQuerySearchQuery(): string {
|
|
const {location} = this.props;
|
|
|
|
return decodeScalar(location.query.query, '').trim();
|
|
}
|
|
|
|
getActiveSort() {
|
|
const {location} = this.props;
|
|
|
|
const urlSort = decodeScalar(location.query.sort, 'myqueries');
|
|
return SORT_OPTIONS.find(item => item.value === urlSort) || SORT_OPTIONS[0];
|
|
}
|
|
|
|
getEndpoints(): ReturnType<DeprecatedAsyncComponent['getEndpoints']> {
|
|
const {organization, location} = this.props;
|
|
|
|
const views = getPrebuiltQueries(organization);
|
|
const searchQuery = this.getSavedQuerySearchQuery();
|
|
|
|
const cursor = decodeScalar(location.query.cursor);
|
|
let perPage = 9;
|
|
|
|
const canRenderPrebuilt = this.state
|
|
? this.state.renderPrebuilt
|
|
: shouldRenderPrebuilt();
|
|
|
|
if (!cursor && canRenderPrebuilt) {
|
|
// invariant: we're on the first page
|
|
|
|
if (searchQuery && searchQuery.length > 0) {
|
|
const needleSearch = searchQuery.toLowerCase();
|
|
|
|
const numOfPrebuiltQueries = views.reduce((sum, view) => {
|
|
const eventView = EventView.fromNewQueryWithLocation(view, location);
|
|
|
|
// if a search is performed on the list of queries, we filter
|
|
// on the pre-built queries
|
|
if (eventView.name?.toLowerCase().includes(needleSearch)) {
|
|
return sum + 1;
|
|
}
|
|
|
|
return sum;
|
|
}, 0);
|
|
|
|
perPage = Math.max(1, perPage - numOfPrebuiltQueries);
|
|
} else {
|
|
perPage = Math.max(1, perPage - views.length);
|
|
}
|
|
}
|
|
|
|
const queryParams: Props['location']['query'] = {
|
|
cursor,
|
|
query: `version:2 name:"${searchQuery}"`,
|
|
per_page: perPage.toString(),
|
|
sortBy: this.getActiveSort().value,
|
|
};
|
|
if (!cursor) {
|
|
delete queryParams.cursor;
|
|
}
|
|
|
|
return [
|
|
[
|
|
'savedQueries',
|
|
`/organizations/${organization.slug}/discover/saved/`,
|
|
{
|
|
query: queryParams,
|
|
},
|
|
],
|
|
];
|
|
}
|
|
|
|
componentDidUpdate(prevProps: Props) {
|
|
const PAYLOAD_KEYS = ['sort', 'cursor', 'query'] as const;
|
|
|
|
const payloadKeysChanged = !isEqual(
|
|
pick(prevProps.location.query, PAYLOAD_KEYS),
|
|
pick(this.props.location.query, PAYLOAD_KEYS)
|
|
);
|
|
|
|
// if any of the query strings relevant for the payload has changed,
|
|
// we re-fetch data
|
|
if (payloadKeysChanged) {
|
|
this.fetchData();
|
|
}
|
|
}
|
|
|
|
handleQueryChange = () => {
|
|
this.fetchData({reloading: true});
|
|
};
|
|
|
|
handleSearchQuery = (searchQuery: string) => {
|
|
const {location} = this.props;
|
|
browserHistory.push({
|
|
pathname: location.pathname,
|
|
query: {
|
|
...location.query,
|
|
cursor: undefined,
|
|
query: String(searchQuery).trim() || undefined,
|
|
},
|
|
});
|
|
};
|
|
|
|
handleSortChange = (value: string) => {
|
|
const {location, organization} = this.props;
|
|
trackAnalytics('discover_v2.change_sort', {organization, sort: value});
|
|
browserHistory.push({
|
|
pathname: location.pathname,
|
|
query: {
|
|
...location.query,
|
|
cursor: undefined,
|
|
sort: value,
|
|
},
|
|
});
|
|
};
|
|
|
|
renderActions() {
|
|
const activeSort = this.getActiveSort();
|
|
const {renderPrebuilt, savedQueries} = this.state;
|
|
|
|
return (
|
|
<StyledActions>
|
|
<StyledSearchBar
|
|
defaultQuery=""
|
|
query={this.getSavedQuerySearchQuery()}
|
|
placeholder={t('Search saved queries')}
|
|
onSearch={this.handleSearchQuery}
|
|
/>
|
|
<PrebuiltSwitch>
|
|
Show Prebuilt
|
|
<Switch
|
|
isActive={renderPrebuilt}
|
|
isDisabled={renderPrebuilt && (savedQueries ?? []).length === 0}
|
|
size="lg"
|
|
toggle={this.togglePrebuilt}
|
|
/>
|
|
</PrebuiltSwitch>
|
|
<CompactSelect
|
|
triggerProps={{prefix: t('Sort By')}}
|
|
value={activeSort.value}
|
|
options={SORT_OPTIONS}
|
|
onChange={opt => this.handleSortChange(opt.value)}
|
|
position="bottom-end"
|
|
/>
|
|
</StyledActions>
|
|
);
|
|
}
|
|
|
|
togglePrebuilt = () => {
|
|
const {renderPrebuilt} = this.state;
|
|
|
|
this.setState({renderPrebuilt: !renderPrebuilt}, () => {
|
|
setRenderPrebuilt(!renderPrebuilt);
|
|
this.fetchData({reloading: true});
|
|
});
|
|
};
|
|
|
|
renderNoAccess() {
|
|
return (
|
|
<Layout.Page withPadding>
|
|
<Alert type="warning">{t("You don't have access to this feature")}</Alert>
|
|
</Layout.Page>
|
|
);
|
|
}
|
|
|
|
renderBody() {
|
|
const {location, organization, router} = this.props;
|
|
const {savedQueries, savedQueriesPageLinks, renderPrebuilt} = this.state;
|
|
|
|
return (
|
|
<QueryList
|
|
pageLinks={savedQueriesPageLinks}
|
|
savedQueries={savedQueries ?? []}
|
|
savedQuerySearchQuery={this.getSavedQuerySearchQuery()}
|
|
renderPrebuilt={renderPrebuilt}
|
|
location={location}
|
|
organization={organization}
|
|
onQueryChange={this.handleQueryChange}
|
|
router={router}
|
|
/>
|
|
);
|
|
}
|
|
|
|
renderBreadcrumbs() {
|
|
return (
|
|
<Breadcrumbs
|
|
crumbs={[
|
|
{
|
|
key: 'discover-homepage',
|
|
label: t('Discover'),
|
|
to: getDiscoverLandingUrl(this.props.organization),
|
|
},
|
|
{
|
|
key: 'discover-saved-queries',
|
|
label: t('Saved Queries'),
|
|
},
|
|
]}
|
|
/>
|
|
);
|
|
}
|
|
|
|
render() {
|
|
const {organization} = this.props;
|
|
const to = `/organizations/${organization.slug}/discover/homepage/`;
|
|
|
|
return (
|
|
<Feature
|
|
organization={organization}
|
|
features="discover-query"
|
|
renderDisabled={this.renderNoAccess}
|
|
>
|
|
<SentryDocumentTitle title={t('Discover')} orgSlug={organization.slug}>
|
|
<Layout.Page>
|
|
<Layout.Header>
|
|
<Layout.HeaderContent>{this.renderBreadcrumbs()}</Layout.HeaderContent>
|
|
<Layout.HeaderActions>
|
|
<Button
|
|
data-test-id="build-new-query"
|
|
to={to}
|
|
size="sm"
|
|
priority="primary"
|
|
onClick={() => {
|
|
trackAnalytics('discover_v2.build_new_query', {
|
|
organization,
|
|
});
|
|
}}
|
|
>
|
|
{t('Build a new query')}
|
|
</Button>
|
|
</Layout.HeaderActions>
|
|
</Layout.Header>
|
|
<Layout.Body>
|
|
<Layout.Main fullWidth>
|
|
{this.renderActions()}
|
|
{this.renderComponent()}
|
|
</Layout.Main>
|
|
</Layout.Body>
|
|
</Layout.Page>
|
|
</SentryDocumentTitle>
|
|
</Feature>
|
|
);
|
|
}
|
|
}
|
|
|
|
const PrebuiltSwitch = styled('label')`
|
|
display: flex;
|
|
align-items: center;
|
|
gap: ${space(1.5)};
|
|
font-weight: normal;
|
|
margin: 0;
|
|
`;
|
|
|
|
const StyledSearchBar = styled(SearchBar)`
|
|
flex-grow: 1;
|
|
`;
|
|
|
|
const StyledActions = styled('div')`
|
|
display: grid;
|
|
gap: ${space(2)};
|
|
grid-template-columns: auto max-content min-content;
|
|
align-items: center;
|
|
margin-bottom: ${space(2)};
|
|
|
|
@media (max-width: ${p => p.theme.breakpoints.small}) {
|
|
grid-template-columns: auto;
|
|
}
|
|
`;
|
|
|
|
export default withOrganization(DiscoverLanding);
|
|
export {DiscoverLanding};
|