You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

352 lines
14 KiB

import React, { useState, useEffect, useCallback } from 'react';
import { useAuth } from '../../../lib/useAuth';
import { Box, CircularProgress, IconButton, Typography, FormControl, Select, MenuItem, TextField, Menu, Button, Card, CardHeader, CardContent, CardActions } from '@material-ui/core';
import { getIntegrations, createIntegration, modifyIntegration, deleteIntegration } from '../../../lib/backend/integrations';
import AddIcon from '@material-ui/icons/Add';
import EditIcon from '@material-ui/icons/Edit';
import CheckIcon from '@material-ui/icons/Check';
import DeleteIcon from '@material-ui/icons/Delete';
import * as serverApi from '../../../api';
import StoreLinkIcon, { ExternalStore } from '../../common/StoreLinkIcon';
import { v4 as genUuid } from 'uuid';
import { testSpotify } from '../../../lib/integration/spotify/spotifyClientCreds';
let _ = require('lodash')
interface EditorIntegrationState extends serverApi.IntegrationDetailsResponse {
secretDetails?: any,
}
interface EditIntegrationProps {
upstreamId: number | null,
integration: EditorIntegrationState,
original: EditorIntegrationState,
editing: boolean,
submitting: boolean,
onChange: (p: EditorIntegrationState, editing: boolean) => void,
onSubmit: () => void,
onDelete: () => void,
}
function EditSpotifyClientCredentialsDetails(props: {
clientId: string,
clientSecret: string | null,
editing: boolean,
onChangeClientId: (v: string) => void,
onChangeClientSecret: (v: string) => void,
}) {
return <Box>
<Box mt={1} mb={1}>
<TextField
variant="outlined"
disabled={!props.editing}
value={props.clientId || ""}
label="Client id"
fullWidth
onChange={(e: any) => props.onChangeClientId(e.target.value)}
/>
</Box>
<Box mt={1} mb={1}>
<TextField
variant="outlined"
disabled={!props.editing}
value={props.clientSecret === null ? "••••••••••••••••" : props.clientSecret}
label="Client secret"
fullWidth
onChange={(e: any) => {
props.onChangeClientSecret(e.target.value)
}}
onFocus={(e: any) => {
if(props.clientSecret === null) {
// Change from dots to empty input
console.log("Focus!")
props.onChangeClientSecret('');
}
}}
/>
</Box>
</Box>;
}
function EditIntegration(props: EditIntegrationProps) {
let IntegrationHeaders: Record<any, any> = {
[serverApi.IntegrationType.SpotifyClientCredentials]:
<Box display="flex" alignItems="center">
<Box mr={1}><StoreLinkIcon
style={{ height: '40px', width: '40px' }}
whichStore={ExternalStore.Spotify}
/></Box>
<Typography>Spotify (using Client Credentials)</Typography>
</Box>
}
let IntegrationDescription: Record<any, any> = {
[serverApi.IntegrationType.SpotifyClientCredentials]:
<Typography>
This integration allows using the Spotify API to make requests that are
tied to any specific user, such as searching items and retrieving item
metadata.<br/>
Please see the Spotify API documentation on how to generate a client ID
and client secret. Once set, you will only be able to overwrite the secret
here, not read it.
</Typography>
}
return <Card variant="outlined">
<CardHeader
avatar={
IntegrationHeaders[props.integration.type]
}
>
</CardHeader>
<CardContent>
<Box mb={2}>{IntegrationDescription[props.integration.type]}</Box>
<Box mt={1} mb={1}>
<TextField
variant="outlined"
value={props.integration.name || ""}
label="Integration name"
fullWidth
disabled={!props.editing}
onChange={(e: any) => props.onChange({
...props.integration,
name: e.target.value,
}, props.editing)}
/>
</Box>
{props.integration.type === serverApi.IntegrationType.SpotifyClientCredentials &&
<EditSpotifyClientCredentialsDetails
clientId={props.integration.details.clientId}
clientSecret={
(props.integration.secretDetails &&
props.integration.secretDetails.clientSecret !== null) ?
props.integration.secretDetails.clientSecret : null}
editing={props.editing}
onChangeClientId={(v: string) => props.onChange({
...props.integration,
details: {
...props.integration.details,
clientId: v,
}
}, props.editing)}
onChangeClientSecret={(v: string) => props.onChange({
...props.integration,
secretDetails: {
...props.integration.secretDetails,
clientSecret: v,
}
}, props.editing)}
/>
}
</CardContent>
<CardActions>
{!props.editing && !props.submitting && <IconButton
onClick={() => { props.onChange(props.integration, true); }}
><EditIcon /></IconButton>}
{props.editing && !props.submitting && <IconButton
onClick={() => { props.onSubmit(); }}
><CheckIcon /></IconButton>}
{!props.submitting && <IconButton
onClick={() => { props.onDelete(); }}
><DeleteIcon /></IconButton>}
{!props.submitting && !props.editing && props.upstreamId !== null && <Button
onClick={() => testSpotify(props.upstreamId || 0)}
>Test</Button>}
{props.submitting && <CircularProgress />}
</CardActions>
</Card>
}
function AddIntegrationMenu(props: {
position: null | number[],
open: boolean,
onClose: () => void,
onAdd: (type: serverApi.IntegrationType) => void,
}) {
const pos = props.open && props.position ?
{ left: props.position[0], top: props.position[1] }
: { left: 0, top: 0 }
return <Menu
open={props.open}
anchorReference="anchorPosition"
anchorPosition={pos}
keepMounted
onClose={props.onClose}
>
<MenuItem
onClick={() => {
props.onAdd(serverApi.IntegrationType.SpotifyClientCredentials);
props.onClose();
}}
>Spotify</MenuItem>
</Menu>
}
export default function IntegrationSettingsEditor(props: {}) {
interface EditorState {
id: string, //uniquely identifies this editor in the window.
upstreamId: number | null, //back-end ID for this integration if any.
integration: EditorIntegrationState,
original: EditorIntegrationState,
editing: boolean,
submitting: boolean,
}
let [editors, setEditors] = useState<EditorState[] | null>(null);
const [addMenuPos, setAddMenuPos] = React.useState<null | number[]>(null);
const onOpenAddMenu = (e: any) => {
setAddMenuPos([e.clientX, e.clientY])
};
const onCloseAddMenu = () => {
setAddMenuPos(null);
};
const submitEditor = (state: EditorState) => {
let integration: any = state.integration;
if (state.upstreamId === null) {
if (!state.integration.secretDetails) {
throw new Error('Cannot create an integration without its secret details set.')
}
createIntegration(integration).then((response: any) => {
if (!response.id) {
throw new Error('failed to submit integration.')
}
let cpy = _.cloneDeep(editors);
cpy.forEach((s: any) => {
if (s.id === state.id) {
s.submitting = false;
s.editing = false;
s.upstreamId = response.id;
}
})
setEditors(cpy);
})
} else {
modifyIntegration(state.upstreamId, integration).then(() => {
let cpy = _.cloneDeep(editors);
cpy.forEach((s: any) => {
if (s.id === state.id) {
s.submitting = false;
s.editing = false;
}
})
setEditors(cpy);
})
}
}
const deleteEditor = (state: EditorState) => {
let promise: Promise<void> = state.upstreamId ?
deleteIntegration(state.upstreamId) :
(async () => { })();
promise.then((response: any) => {
let cpy = _.cloneDeep(editors).filter(
(e: any) => e.id !== state.id
);
setEditors(cpy);
})
}
useEffect(() => {
getIntegrations()
.then((integrations: serverApi.ListIntegrationsResponse) => {
setEditors(integrations.map((i: any, idx: any) => {
return {
integration: { ...i },
original: { ...i },
id: genUuid(),
editing: false,
submitting: false,
upstreamId: i.id,
}
}));
});
}, []);
return <>
<Box>
{editors === null && <CircularProgress />}
{editors && <Box display="flex" flexDirection="column" alignItems="center" flexWrap="wrap">
{editors.map((state: EditorState) => <Box m={1} width="90%">
<EditIntegration
upstreamId={state.upstreamId}
integration={state.integration}
original={state.original}
editing={state.editing}
submitting={state.submitting}
onChange={(p: EditorIntegrationState, editing: boolean) => {
if (!editors) {
throw new Error('cannot change editors before loading integrations.')
}
let cpy: EditorState[] = _.cloneDeep(editors);
cpy.forEach((s: any) => {
if (s.id === state.id) {
s.integration = p;
s.editing = editing;
}
})
setEditors(cpy);
}}
onSubmit={() => {
if (!editors) {
throw new Error('cannot submit editors before loading integrations.')
}
let cpy: EditorState[] = _.cloneDeep(editors);
cpy.forEach((s: EditorState) => {
if (s.id === state.id) {
s.submitting = true;
s.integration.secretDetails = undefined;
}
})
setEditors(cpy);
submitEditor(state);
}}
onDelete={() => {
if (!editors) {
throw new Error('cannot submit editors before loading integrations.')
}
let cpy: EditorState[] = _.cloneDeep(editors);
cpy.forEach((s: any) => {
if (s.id === state.id) {
s.submitting = true;
}
})
setEditors(cpy);
deleteEditor(state);
}}
/>
</Box>)}
<IconButton onClick={onOpenAddMenu}>
<AddIcon />
</IconButton>
</Box>}
</Box>
<AddIntegrationMenu
position={addMenuPos}
open={addMenuPos !== null}
onClose={onCloseAddMenu}
onAdd={(type: serverApi.IntegrationType) => {
let cpy = _.cloneDeep(editors);
cpy.push({
integration: {
type: serverApi.IntegrationType.SpotifyClientCredentials,
details: {
clientId: '',
},
secretDetails: {
clientSecret: '',
},
name: '',
},
original: null,
id: genUuid(),
editing: true,
submitting: false,
upstreamId: null,
})
setEditors(cpy);
}}
/>
</>;
}