|
@@ -1,15 +1,23 @@
|
|
|
import {Fragment, useEffect, useState} from 'react';
|
|
|
+import {css} from '@emotion/react';
|
|
|
import styled from '@emotion/styled';
|
|
|
-import partition from 'lodash/partition';
|
|
|
|
|
|
import {openModal} from 'sentry/actionCreators/modal';
|
|
|
+import Button from 'sentry/components/button';
|
|
|
+import ButtonBar from 'sentry/components/buttonBar';
|
|
|
import LoadingError from 'sentry/components/loadingError';
|
|
|
import LoadingIndicator from 'sentry/components/loadingIndicator';
|
|
|
-import {PanelTable} from 'sentry/components/panels';
|
|
|
+import {Panel, PanelFooter, PanelHeader} from 'sentry/components/panels';
|
|
|
import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
|
|
|
+import {IconAdd} from 'sentry/icons';
|
|
|
import {t} from 'sentry/locale';
|
|
|
+import space from 'sentry/styles/space';
|
|
|
import {Project} from 'sentry/types';
|
|
|
-import {SamplingRules, SamplingRuleType} from 'sentry/types/sampling';
|
|
|
+import {
|
|
|
+ SamplingRuleOperator,
|
|
|
+ SamplingRules,
|
|
|
+ SamplingRuleType,
|
|
|
+} from 'sentry/types/sampling';
|
|
|
import handleXhrErrorResponse from 'sentry/utils/handleXhrErrorResponse';
|
|
|
import useApi from 'sentry/utils/useApi';
|
|
|
import useOrganization from 'sentry/utils/useOrganization';
|
|
@@ -18,8 +26,20 @@ import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHea
|
|
|
import TextBlock from 'sentry/views/settings/components/text/textBlock';
|
|
|
import PermissionAlert from 'sentry/views/settings/organization/permissionAlert';
|
|
|
|
|
|
+import {DraggableList} from '../sampling/rules/draggableList';
|
|
|
+
|
|
|
import {UniformRateModal} from './modals/uniformRateModal';
|
|
|
import {Promo} from './promo';
|
|
|
+import {
|
|
|
+ ActiveColumn,
|
|
|
+ Column,
|
|
|
+ ConditionColumn,
|
|
|
+ GrabColumn,
|
|
|
+ OperatorColumn,
|
|
|
+ RateColumn,
|
|
|
+ Rule,
|
|
|
+} from './rule';
|
|
|
+import {SERVER_SIDE_SAMPLING_DOC_LINK} from './utils';
|
|
|
|
|
|
export function ServerSideSampling() {
|
|
|
const api = useApi();
|
|
@@ -29,7 +49,7 @@ export function ServerSideSampling() {
|
|
|
|
|
|
const {orgId: orgSlug, projectId: projectSlug} = params;
|
|
|
|
|
|
- const [_rules, setRules] = useState<SamplingRules>([]);
|
|
|
+ const [rules, setRules] = useState<SamplingRules>([]);
|
|
|
const [project, setProject] = useState<Project>();
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
const [error, setError] = useState<string | undefined>(undefined);
|
|
@@ -45,18 +65,11 @@ export function ServerSideSampling() {
|
|
|
const {dynamicSampling} = projectDetails;
|
|
|
const samplingRules: SamplingRules = dynamicSampling?.rules ?? [];
|
|
|
|
|
|
- const transactionRules = samplingRules.filter(
|
|
|
- samplingRule =>
|
|
|
- samplingRule.type === SamplingRuleType.TRANSACTION ||
|
|
|
- samplingRule.type === SamplingRuleType.TRACE
|
|
|
- );
|
|
|
-
|
|
|
- const [rulesWithoutConditions, rulesWithConditions] = partition(
|
|
|
- transactionRules,
|
|
|
- transactionRule => !transactionRule.condition.inner.length
|
|
|
+ const traceRules = samplingRules.filter(
|
|
|
+ samplingRule => samplingRule.type === SamplingRuleType.TRACE
|
|
|
);
|
|
|
|
|
|
- setRules([...rulesWithConditions, ...rulesWithoutConditions]);
|
|
|
+ setRules(traceRules);
|
|
|
setProject(projectDetails);
|
|
|
setLoading(false);
|
|
|
} catch (err) {
|
|
@@ -75,6 +88,14 @@ export function ServerSideSampling() {
|
|
|
));
|
|
|
}
|
|
|
|
|
|
+ // Rules without a condition (Else case) always have to be 'pinned' to the bottom of the list
|
|
|
+ // and cannot be sorted
|
|
|
+ const items = rules.map(rule => ({
|
|
|
+ ...rule,
|
|
|
+ id: String(rule.id),
|
|
|
+ bottomPinned: !rule.condition.inner.length,
|
|
|
+ }));
|
|
|
+
|
|
|
return (
|
|
|
<SentryDocumentTitle title={t('Server-side Sampling')}>
|
|
|
<Fragment>
|
|
@@ -93,10 +114,112 @@ export function ServerSideSampling() {
|
|
|
{error && <LoadingError message={error} />}
|
|
|
{!error && loading && <LoadingIndicator />}
|
|
|
{!error && !loading && (
|
|
|
- <RulesPanel
|
|
|
- headers={['', t('Operator'), t('Condition'), t('Rate'), t('Active'), '']}
|
|
|
- >
|
|
|
- <Promo onGetStarted={handleGetStarted} hasAccess={hasAccess} />
|
|
|
+ <RulesPanel>
|
|
|
+ <RulesPanelHeader lightText>
|
|
|
+ <RulesPanelLayout>
|
|
|
+ <GrabColumn />
|
|
|
+ <OperatorColumn>{t('Operator')}</OperatorColumn>
|
|
|
+ <ConditionColumn>{t('Condition')}</ConditionColumn>
|
|
|
+ <RateColumn>{t('Rate')}</RateColumn>
|
|
|
+ <ActiveColumn>{t('Active')}</ActiveColumn>
|
|
|
+ <Column />
|
|
|
+ </RulesPanelLayout>
|
|
|
+ </RulesPanelHeader>
|
|
|
+ {!rules.length && (
|
|
|
+ <Promo onGetStarted={handleGetStarted} hasAccess={hasAccess} />
|
|
|
+ )}
|
|
|
+ {!!rules.length && (
|
|
|
+ <Fragment>
|
|
|
+ <DraggableList
|
|
|
+ disabled={!hasAccess}
|
|
|
+ items={items}
|
|
|
+ onUpdateItems={() => {}}
|
|
|
+ wrapperStyle={({isDragging, isSorting, index}) => {
|
|
|
+ if (isDragging) {
|
|
|
+ return {
|
|
|
+ cursor: 'grabbing',
|
|
|
+ };
|
|
|
+ }
|
|
|
+ if (isSorting) {
|
|
|
+ return {};
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ transform: 'none',
|
|
|
+ transformOrigin: '0',
|
|
|
+ '--box-shadow': 'none',
|
|
|
+ '--box-shadow-picked-up': 'none',
|
|
|
+ overflow: 'visible',
|
|
|
+ position: 'relative',
|
|
|
+ zIndex: rules.length - index,
|
|
|
+ cursor: 'default',
|
|
|
+ };
|
|
|
+ }}
|
|
|
+ renderItem={({value, listeners, attributes, dragging, sorting}) => {
|
|
|
+ const itemsRuleIndex = items.findIndex(item => item.id === value);
|
|
|
+
|
|
|
+ if (itemsRuleIndex === -1) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ const itemsRule = items[itemsRuleIndex];
|
|
|
+
|
|
|
+ const currentRule = {
|
|
|
+ active: itemsRule.active,
|
|
|
+ condition: itemsRule.condition,
|
|
|
+ sampleRate: itemsRule.sampleRate,
|
|
|
+ type: itemsRule.type,
|
|
|
+ id: Number(itemsRule.id),
|
|
|
+ };
|
|
|
+
|
|
|
+ return (
|
|
|
+ <RulesPanelLayout isContent>
|
|
|
+ <Rule
|
|
|
+ operator={
|
|
|
+ itemsRule.id === items[0].id
|
|
|
+ ? SamplingRuleOperator.IF
|
|
|
+ : itemsRule.bottomPinned
|
|
|
+ ? SamplingRuleOperator.ELSE
|
|
|
+ : SamplingRuleOperator.ELSE_IF
|
|
|
+ }
|
|
|
+ hideGrabButton={items.length === 1}
|
|
|
+ rule={{
|
|
|
+ ...currentRule,
|
|
|
+ bottomPinned: itemsRule.bottomPinned,
|
|
|
+ }}
|
|
|
+ onEditRule={() => {}}
|
|
|
+ onDeleteRule={() => {}}
|
|
|
+ noPermission={!hasAccess}
|
|
|
+ listeners={listeners}
|
|
|
+ grabAttributes={attributes}
|
|
|
+ dragging={dragging}
|
|
|
+ sorting={sorting}
|
|
|
+ />
|
|
|
+ </RulesPanelLayout>
|
|
|
+ );
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ <RulesPanelFooter>
|
|
|
+ <ButtonBar gap={1}>
|
|
|
+ <Button href={SERVER_SIDE_SAMPLING_DOC_LINK} external>
|
|
|
+ {t('Read Docs')}
|
|
|
+ </Button>
|
|
|
+ <AddRuleButton
|
|
|
+ disabled={!hasAccess}
|
|
|
+ title={
|
|
|
+ !hasAccess
|
|
|
+ ? t("You don't have permission to add a rule")
|
|
|
+ : undefined
|
|
|
+ }
|
|
|
+ priority="primary"
|
|
|
+ onClick={() => {}}
|
|
|
+ icon={<IconAdd isCircled />}
|
|
|
+ >
|
|
|
+ {t('Add Rule')}
|
|
|
+ </AddRuleButton>
|
|
|
+ </ButtonBar>
|
|
|
+ </RulesPanelFooter>
|
|
|
+ </Fragment>
|
|
|
+ )}
|
|
|
</RulesPanel>
|
|
|
)}
|
|
|
</Fragment>
|
|
@@ -104,36 +227,44 @@ export function ServerSideSampling() {
|
|
|
);
|
|
|
}
|
|
|
|
|
|
-const RulesPanel = styled(PanelTable)`
|
|
|
- > * {
|
|
|
- :not(:last-child) {
|
|
|
- border-bottom: 1px solid ${p => p.theme.border};
|
|
|
- }
|
|
|
-
|
|
|
- :nth-child(-n + 6):nth-child(6n - 1) {
|
|
|
- text-align: right;
|
|
|
- }
|
|
|
+const RulesPanel = styled(Panel)``;
|
|
|
|
|
|
- @media (max-width: ${p => p.theme.breakpoints.small}) {
|
|
|
- :nth-child(6n - 1),
|
|
|
- :nth-child(6n - 4),
|
|
|
- :nth-child(6n - 5) {
|
|
|
- display: none;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+const RulesPanelHeader = styled(PanelHeader)`
|
|
|
+ padding: ${space(0.5)} 0;
|
|
|
+ font-size: ${p => p.theme.fontSizeSmall};
|
|
|
+`;
|
|
|
|
|
|
- grid-template-columns: 1fr 0.5fr 66px;
|
|
|
+const RulesPanelLayout = styled('div')<{isContent?: boolean}>`
|
|
|
+ width: 100%;
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 1fr 0.5fr 74px;
|
|
|
|
|
|
@media (min-width: ${p => p.theme.breakpoints.small}) {
|
|
|
- grid-template-columns: 48px 95px 1fr 0.5fr 77px 66px;
|
|
|
+ grid-template-columns: 48px 95px 1fr 0.5fr 77px 74px;
|
|
|
}
|
|
|
|
|
|
- @media (min-width: ${p => p.theme.breakpoints.large}) {
|
|
|
- grid-template-columns: 48px 95px 1.5fr 1fr 77px 124px;
|
|
|
- }
|
|
|
+ ${p =>
|
|
|
+ p.isContent &&
|
|
|
+ css`
|
|
|
+ > * {
|
|
|
+ /* match the height of the ellipsis button */
|
|
|
+ line-height: 34px;
|
|
|
+ border-bottom: 1px solid ${p.theme.border};
|
|
|
+ }
|
|
|
+ `}
|
|
|
+`;
|
|
|
+
|
|
|
+const RulesPanelFooter = styled(PanelFooter)`
|
|
|
+ border-top: none;
|
|
|
+ padding: ${space(1.5)} ${space(2)};
|
|
|
+ grid-column: 1 / -1;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: flex-end;
|
|
|
+`;
|
|
|
|
|
|
- @media (min-width: ${p => p.theme.breakpoints.xlarge}) {
|
|
|
- grid-template-columns: 48px 95px 1fr 0.5fr 77px 124px;
|
|
|
+const AddRuleButton = styled(Button)`
|
|
|
+ @media (max-width: ${p => p.theme.breakpoints.small}) {
|
|
|
+ width: 100%;
|
|
|
}
|
|
|
`;
|