mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
300 lines
8.5 KiB
TypeScript
300 lines
8.5 KiB
TypeScript
import {Component} from 'react';
|
|
import styled from '@emotion/styled';
|
|
|
|
import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
|
|
import type {Client} from 'sentry/api';
|
|
import Avatar from 'sentry/components/avatar';
|
|
import {AvatarUploader} from 'sentry/components/avatarUploader';
|
|
import {Button} from 'sentry/components/button';
|
|
import RadioGroup from 'sentry/components/forms/controls/radioGroup';
|
|
import ExternalLink from 'sentry/components/links/externalLink';
|
|
import LoadingError from 'sentry/components/loadingError';
|
|
import LoadingIndicator from 'sentry/components/loadingIndicator';
|
|
import Panel from 'sentry/components/panels/panel';
|
|
import PanelBody from 'sentry/components/panels/panelBody';
|
|
import PanelHeader from 'sentry/components/panels/panelHeader';
|
|
import Well from 'sentry/components/well';
|
|
import {t} from 'sentry/locale';
|
|
import {space} from 'sentry/styles/space';
|
|
import type {AvatarUser, Organization, SentryApp, Team} from 'sentry/types';
|
|
import withApi from 'sentry/utils/withApi';
|
|
|
|
export type Model = Pick<AvatarUser, 'avatar'>;
|
|
type AvatarType = Required<Model>['avatar']['avatarType'];
|
|
type AvatarChooserType =
|
|
| 'user'
|
|
| 'team'
|
|
| 'organization'
|
|
| 'sentryAppColor'
|
|
| 'sentryAppSimple'
|
|
| 'docIntegration';
|
|
type DefaultChoice = {
|
|
allowDefault?: boolean;
|
|
choiceText?: string;
|
|
preview?: React.ReactNode;
|
|
};
|
|
|
|
type DefaultProps = {
|
|
onSave: (model: Model) => void;
|
|
allowGravatar?: boolean;
|
|
allowLetter?: boolean;
|
|
allowUpload?: boolean;
|
|
defaultChoice?: DefaultChoice;
|
|
type?: AvatarChooserType;
|
|
uploadDomain?: string;
|
|
};
|
|
|
|
type Props = {
|
|
api: Client;
|
|
endpoint: string;
|
|
model: Model;
|
|
disabled?: boolean;
|
|
help?: React.ReactNode;
|
|
isUser?: boolean;
|
|
savedDataUrl?: string;
|
|
title?: string;
|
|
} & DefaultProps;
|
|
|
|
type State = {
|
|
hasError: boolean;
|
|
model: Model;
|
|
dataUrl?: string | null;
|
|
savedDataUrl?: string | null;
|
|
};
|
|
|
|
class AvatarChooser extends Component<Props, State> {
|
|
static defaultProps: DefaultProps = {
|
|
allowGravatar: true,
|
|
allowLetter: true,
|
|
allowUpload: true,
|
|
type: 'user',
|
|
onSave: () => {},
|
|
defaultChoice: {
|
|
allowDefault: false,
|
|
},
|
|
uploadDomain: '',
|
|
};
|
|
|
|
state: State = {
|
|
model: this.props.model,
|
|
savedDataUrl: null,
|
|
dataUrl: null,
|
|
hasError: false,
|
|
};
|
|
|
|
componentDidUpdate(prevProps: Props) {
|
|
const {model} = this.props;
|
|
|
|
// Update local state if defined in props
|
|
if (model !== undefined && model !== prevProps.model) {
|
|
this.setState({model});
|
|
}
|
|
}
|
|
|
|
getModelFromResponse(resp: any): Model {
|
|
const {type} = this.props;
|
|
const isSentryApp = type?.startsWith('sentryApp');
|
|
// SentryApp endpoint returns all avatars, we need to return only the edited one
|
|
if (!isSentryApp) {
|
|
return resp;
|
|
}
|
|
const isColor = type === 'sentryAppColor';
|
|
return {avatar: resp?.avatars?.find(({color}) => color === isColor) ?? undefined};
|
|
}
|
|
|
|
handleError(msg: string) {
|
|
addErrorMessage(msg);
|
|
}
|
|
|
|
handleSuccess(model: Model) {
|
|
const {onSave} = this.props;
|
|
this.setState({model});
|
|
onSave(model);
|
|
addSuccessMessage(t('Successfully saved avatar preferences'));
|
|
}
|
|
|
|
handleSaveSettings = (ev: React.MouseEvent) => {
|
|
const {endpoint, api, type} = this.props;
|
|
const {model, dataUrl} = this.state;
|
|
|
|
ev.preventDefault();
|
|
const avatarType = model?.avatar?.avatarType;
|
|
const avatarPhoto = dataUrl?.split(',')[1];
|
|
|
|
const data: {
|
|
avatar_photo?: string;
|
|
avatar_type?: string;
|
|
color?: boolean;
|
|
} = {avatar_type: avatarType};
|
|
|
|
// If an image has been uploaded, then another option is selected, we should not submit the uploaded image
|
|
if (avatarType === 'upload') {
|
|
data.avatar_photo = avatarPhoto;
|
|
}
|
|
|
|
if (type?.startsWith('sentryApp')) {
|
|
data.color = type === 'sentryAppColor';
|
|
}
|
|
|
|
api.request(endpoint, {
|
|
method: 'PUT',
|
|
data,
|
|
success: resp => {
|
|
this.setState({savedDataUrl: this.state.dataUrl});
|
|
this.handleSuccess(this.getModelFromResponse(resp));
|
|
},
|
|
error: resp => {
|
|
const avatarPhotoErrors = resp?.responseJSON?.avatar_photo || [];
|
|
avatarPhotoErrors.length
|
|
? avatarPhotoErrors.map(this.handleError)
|
|
: this.handleError.bind(this, t('There was an error saving your preferences.'));
|
|
},
|
|
});
|
|
};
|
|
|
|
handleChange = (id: AvatarType) =>
|
|
this.setState(state => ({
|
|
model: {
|
|
...state.model,
|
|
avatar: {avatarUuid: state.model.avatar?.avatarUuid ?? '', avatarType: id},
|
|
},
|
|
}));
|
|
|
|
render() {
|
|
const {
|
|
allowGravatar,
|
|
allowUpload,
|
|
allowLetter,
|
|
savedDataUrl,
|
|
type,
|
|
isUser,
|
|
disabled,
|
|
title,
|
|
help,
|
|
defaultChoice,
|
|
uploadDomain,
|
|
} = this.props;
|
|
const {hasError, model, dataUrl} = this.state;
|
|
|
|
if (hasError) {
|
|
return <LoadingError />;
|
|
}
|
|
if (!model) {
|
|
return <LoadingIndicator />;
|
|
}
|
|
const {allowDefault, preview, choiceText: defaultChoiceText} = defaultChoice || {};
|
|
|
|
const avatarType = model.avatar?.avatarType ?? 'letter_avatar';
|
|
const isLetter = avatarType === 'letter_avatar';
|
|
const isDefault = Boolean(preview && avatarType === 'default');
|
|
|
|
const isTeam = type === 'team';
|
|
const isOrganization = type === 'organization';
|
|
const isSentryApp = type?.startsWith('sentryApp');
|
|
|
|
const choices: [AvatarType, string][] = [];
|
|
|
|
if (allowDefault && preview) {
|
|
choices.push(['default', defaultChoiceText ?? t('Use default avatar')]);
|
|
}
|
|
if (allowLetter) {
|
|
choices.push(['letter_avatar', t('Use initials')]);
|
|
}
|
|
if (allowUpload) {
|
|
choices.push(['upload', t('Upload an image')]);
|
|
}
|
|
if (allowGravatar) {
|
|
choices.push(['gravatar', t('Use Gravatar')]);
|
|
}
|
|
return (
|
|
<Panel>
|
|
<PanelHeader>{title || t('Avatar')}</PanelHeader>
|
|
<PanelBody>
|
|
<AvatarForm>
|
|
<AvatarGroup inline={isLetter || isDefault}>
|
|
<RadioGroup
|
|
style={{flex: 1}}
|
|
choices={choices}
|
|
value={avatarType}
|
|
label={t('Avatar Type')}
|
|
onChange={this.handleChange}
|
|
disabled={disabled}
|
|
/>
|
|
{isLetter && (
|
|
<Avatar
|
|
gravatar={false}
|
|
style={{width: 90, height: 90}}
|
|
user={isUser ? (model as AvatarUser) : undefined}
|
|
organization={isOrganization ? (model as Organization) : undefined}
|
|
team={isTeam ? (model as Team) : undefined}
|
|
sentryApp={isSentryApp ? (model as SentryApp) : undefined}
|
|
/>
|
|
)}
|
|
{isDefault && preview}
|
|
</AvatarGroup>
|
|
<AvatarUploadSection>
|
|
{allowGravatar && avatarType === 'gravatar' && (
|
|
<Well>
|
|
{t('Gravatars are managed through ')}
|
|
<ExternalLink href="http://gravatar.com">Gravatar.com</ExternalLink>
|
|
</Well>
|
|
)}
|
|
{model.avatar && avatarType === 'upload' && (
|
|
<AvatarUploader
|
|
{...this.props}
|
|
type={type!}
|
|
model={model}
|
|
savedDataUrl={savedDataUrl}
|
|
uploadDomain={uploadDomain ?? ''}
|
|
updateDataUrlState={dataState => this.setState(dataState)}
|
|
/>
|
|
)}
|
|
<AvatarSubmit className="form-actions">
|
|
{help && <AvatarHelp>{help}</AvatarHelp>}
|
|
<Button
|
|
priority="primary"
|
|
onClick={this.handleSaveSettings}
|
|
disabled={disabled || (avatarType === 'upload' && !dataUrl)}
|
|
>
|
|
{t('Save Avatar')}
|
|
</Button>
|
|
</AvatarSubmit>
|
|
</AvatarUploadSection>
|
|
</AvatarForm>
|
|
</PanelBody>
|
|
</Panel>
|
|
);
|
|
}
|
|
}
|
|
|
|
const AvatarHelp = styled('p')`
|
|
margin-right: auto;
|
|
color: ${p => p.theme.gray300};
|
|
font-size: 14px;
|
|
width: 50%;
|
|
`;
|
|
|
|
const AvatarGroup = styled('div')<{inline: boolean}>`
|
|
display: flex;
|
|
flex-direction: ${p => (p.inline ? 'row' : 'column')};
|
|
`;
|
|
|
|
const AvatarForm = styled('div')`
|
|
line-height: ${space(3)};
|
|
padding: ${space(1.5)} ${space(2)};
|
|
margin: ${space(1.5)} ${space(1)} ${space(0.5)};
|
|
`;
|
|
|
|
const AvatarSubmit = styled('fieldset')`
|
|
display: flex;
|
|
align-items: center;
|
|
margin-top: ${space(4)};
|
|
padding-top: ${space(1.5)};
|
|
`;
|
|
|
|
const AvatarUploadSection = styled('div')`
|
|
margin-top: ${space(1.5)};
|
|
`;
|
|
|
|
export default withApi(AvatarChooser);
|