show delete integration btn only if allow_delete is truthy (#3377)

# What this PR does
Show delete integration btn only if `allow_delete` is `true` for it


## Which issue(s) this PR fixes
https://github.com/grafana/oncall-private/issues/2302


## Checklist

- [x] Unit, integration, and e2e (if applicable) tests updated
- [x] Documentation added (or `pr:no public docs` PR label added if not
required)
- [x] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not
required)
This commit is contained in:
Dominik Broj 2023-11-17 14:38:20 +01:00 committed by GitHub
parent 8f13e312f7
commit 53ca5331c6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 44 additions and 208 deletions

View file

@ -0,0 +1,11 @@
import React, { FC, ReactNode } from 'react';
interface RenderConditionallyProps {
shouldRender?: boolean;
children: ReactNode;
}
const RenderConditionally: FC<RenderConditionallyProps> = ({ shouldRender, children }) =>
shouldRender ? <>{children}</> : null;
export default RenderConditionally;

View file

@ -1,58 +0,0 @@
.modal {
width: 840px;
}
.cards {
display: flex;
flex-wrap: wrap;
gap: 24px;
max-height: 550px;
overflow: auto;
scroll-snap-type: y mandatory;
padding: 0 10px 10px 0;
min-width: 840px;
}
.cards_centered {
justify-content: center;
align-items: center;
}
.card {
width: 240px;
height: 88px;
scroll-snap-align: start;
scroll-snap-stop: normal;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
cursor: pointer;
position: relative;
gap: 20px;
}
.card_featured {
width: 768px;
}
.tag {
top: 28px;
right: 28px;
position: absolute;
}
.title {
margin: 10px 0 10px 0;
max-width: 500px;
}
.footer {
display: block;
margin-top: 10px;
}
.search-integration {
width: 400px;
margin-bottom: 24px;
}

View file

@ -1,118 +0,0 @@
import React, { ChangeEvent, useCallback, useState } from 'react';
import { EmptySearchResult, HorizontalGroup, Input, Modal, VerticalGroup, Tag, Field } from '@grafana/ui';
import cn from 'classnames/bind';
import { observer } from 'mobx-react';
import Block from 'components/GBlock/Block';
import IntegrationLogo from 'components/IntegrationLogo/IntegrationLogo';
import Text from 'components/Text/Text';
import GrafanaTeamSelect from 'containers/GrafanaTeamSelect/GrafanaTeamSelect';
import { AlertReceiveChannelOption } from 'models/alert_receive_channel/alert_receive_channel.types';
import { GrafanaTeam } from 'models/grafana_team/grafana_team.types';
import { useStore } from 'state/useStore';
import styles from './CreateAlertReceiveChannelContainer.module.css';
const cx = cn.bind(styles);
interface CreateAlertReceiveChannelContainerProps {
onHide: () => void;
onCreate: (option: AlertReceiveChannelOption, team: GrafanaTeam['id']) => void;
}
const CreateAlertReceiveChannelContainer = observer((props: CreateAlertReceiveChannelContainerProps) => {
const { onHide, onCreate } = props;
const { alertReceiveChannelStore, userStore } = useStore();
const user = userStore.currentUser;
const [filterValue, setFilterValue] = useState('');
const [selectedTeam, setSelectedTeam] = useState<GrafanaTeam['id']>(user.current_team);
const handleNewIntegrationOptionSelectCallback = useCallback(
(option: AlertReceiveChannelOption) => {
onHide();
onCreate(option, selectedTeam);
},
[onCreate, onHide, selectedTeam]
);
const handleChangeFilter = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setFilterValue(e.currentTarget.value);
}, []);
const { alertReceiveChannelOptions } = alertReceiveChannelStore;
const options = alertReceiveChannelOptions
? alertReceiveChannelOptions.filter((option: AlertReceiveChannelOption) =>
option.display_name.toLowerCase().includes(filterValue.toLowerCase())
)
: [];
return (
<Modal
title={
<HorizontalGroup className={cx('title')}>
<Text.Title level={4}> Create Integration</Text.Title>
</HorizontalGroup>
}
isOpen
closeOnEscape={false}
onDismiss={onHide}
className={cx('modal')}
>
<div className={cx('select-team')}>
<Field
label="Assign to team"
description="OnCall teams allow you to organize integrations so you can filter and set up access. "
>
<GrafanaTeamSelect withoutModal onSelect={setSelectedTeam} defaultValue={user.current_team} />
</Field>
</div>
<hr />
<div className={cx('search-integration')}>
<Input autoFocus value={filterValue} placeholder="Search integrations ..." onChange={handleChangeFilter} />
</div>
<div className={cx('cards', { cards_centered: !options.length })} data-testid="create-integration-modal">
{options.length ? (
options.map((alertReceiveChannelChoice) => {
return (
<Block
bordered
shadowed
onClick={() => {
handleNewIntegrationOptionSelectCallback(alertReceiveChannelChoice);
}}
key={alertReceiveChannelChoice.value}
className={cx('card', { card_featured: alertReceiveChannelChoice.featured })}
>
<div className={cx('card-bg')}>
<IntegrationLogo integration={alertReceiveChannelChoice} scale={0.2} />
</div>
<div className={cx('title')}>
<VerticalGroup spacing="none">
<Text strong data-testid="integration-display-name">
{alertReceiveChannelChoice.display_name}
</Text>
<Text type="secondary" size="small">
{alertReceiveChannelChoice.short_description}
</Text>
</VerticalGroup>
</div>
{alertReceiveChannelChoice.featured && (
<Tag name="Quick connect" className={cx('tag')} colorIndex={7} />
)}
</Block>
);
})
) : (
<EmptySearchResult>Could not find anything matching your query</EmptySearchResult>
)}
</div>
</Modal>
);
});
export default CreateAlertReceiveChannelContainer;

View file

@ -29,6 +29,7 @@ import {
initErrorDataState,
} from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.helpers';
import PluginLink from 'components/PluginLink/PluginLink';
import RenderConditionally from 'components/RenderConditionally/RenderConditionally';
import Text from 'components/Text/Text';
import TextEllipsisTooltip from 'components/TextEllipsisTooltip/TextEllipsisTooltip';
import TooltipBadge from 'components/TooltipBadge/TooltipBadge';
@ -540,39 +541,39 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
</HorizontalGroup>
</div>
</CopyToClipboard>
<div className={cx('thin-line-break')} />
<WithPermissionControlTooltip key="delete" userAction={UserActions.IntegrationsWrite}>
<div className={cx('integrations-actionItem')}>
<div
onClick={() => {
this.setState({
confirmationModal: {
isOpen: true,
confirmText: 'Delete',
dismissText: 'Cancel',
onConfirm: () => this.handleDeleteAlertReceiveChannel(item.id),
title: 'Delete integration',
body: (
<Text type="primary">
Are you sure you want to delete <Emoji text={item.verbal_name} /> integration?
</Text>
),
},
});
}}
style={{ width: '100%' }}
>
<Text type="danger">
<HorizontalGroup spacing={'xs'}>
<Icon name="trash-alt" />
<span>Delete Integration</span>
</HorizontalGroup>
</Text>
<RenderConditionally shouldRender={item.allow_delete}>
<div className={cx('thin-line-break')} />
<WithPermissionControlTooltip key="delete" userAction={UserActions.IntegrationsWrite}>
<div className={cx('integrations-actionItem')}>
<div
onClick={() => {
this.setState({
confirmationModal: {
isOpen: true,
confirmText: 'Delete',
dismissText: 'Cancel',
onConfirm: () => this.handleDeleteAlertReceiveChannel(item.id),
title: 'Delete integration',
body: (
<Text type="primary">
Are you sure you want to delete <Emoji text={item.verbal_name} /> integration?
</Text>
),
},
});
}}
className="u-width-100"
>
<Text type="danger">
<HorizontalGroup spacing={'xs'}>
<Icon name="trash-alt" />
<span>Delete Integration</span>
</HorizontalGroup>
</Text>
</div>
</div>
</div>
</WithPermissionControlTooltip>
</WithPermissionControlTooltip>
</RenderConditionally>
</div>
)}
>