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
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); |
|
}} |
|
/> |
|
</>; |
|
} |