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.
354 lines
14 KiB
354 lines
14 KiB
import React, { useState, useEffect } from 'react'; |
|
import { Box, CircularProgress, IconButton, Typography, MenuItem, TextField, Menu, Button, Card, CardHeader, CardContent, CardActions, Dialog, DialogTitle } 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 ClearIcon from '@material-ui/icons/Clear'; |
|
import * as serverApi from '../../../api/api'; |
|
import { v4 as genUuid } from 'uuid'; |
|
import { useIntegrations, IntegrationClasses, IntegrationState, isIntegrationState, makeDefaultIntegrationProperties, makeIntegration } from '../../../lib/integration/useIntegrations'; |
|
import Alert from '@material-ui/lab/Alert'; |
|
import Integration from '../../../lib/integration/Integration'; |
|
let _ = require('lodash') |
|
|
|
// This widget is used to either display or edit a few |
|
// specifically needed for Spotify Client credentials integration. |
|
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>; |
|
} |
|
|
|
// An editing widget which is meant to either display or edit properties |
|
// of an integration. |
|
function EditIntegration(props: { |
|
upstreamId?: number, |
|
integration: serverApi.PostIntegrationRequest, |
|
editing?: boolean, |
|
showSubmitButton?: boolean | "InProgress", |
|
showDeleteButton?: boolean | "InProgress", |
|
showEditButton?: boolean, |
|
showTestButton?: boolean | "InProgress", |
|
showCancelButton?: boolean, |
|
flashMessage?: React.ReactFragment, |
|
isNew: boolean, |
|
onChange?: (p: serverApi.PostIntegrationRequest) => void, |
|
onSubmit?: (p: serverApi.PostIntegrationRequest) => void, |
|
onDelete?: () => void, |
|
onEdit?: () => void, |
|
onTest?: () => void, |
|
onCancel?: () => void, |
|
}) { |
|
let IntegrationHeaders: Record<any, any> = { |
|
[serverApi.IntegrationImpl.SpotifyClientCredentials]: |
|
<Box display="flex" alignItems="center"> |
|
<Box mr={1}> |
|
{new IntegrationClasses[serverApi.IntegrationImpl.SpotifyClientCredentials](-1).getIcon({ |
|
style: { height: '40px', width: '40px' } |
|
})} |
|
</Box> |
|
<Typography>Spotify (using Client Credentials)</Typography> |
|
</Box>, |
|
[serverApi.IntegrationImpl.YoutubeWebScraper]: |
|
<Box display="flex" alignItems="center"> |
|
<Box mr={1}> |
|
{new IntegrationClasses[serverApi.IntegrationImpl.YoutubeWebScraper](-1).getIcon({ |
|
style: { height: '40px', width: '40px' } |
|
})} |
|
</Box> |
|
<Typography>Youtube Music (using experimental web scraper)</Typography> |
|
</Box>, |
|
} |
|
let IntegrationDescription: Record<any, any> = { |
|
[serverApi.IntegrationImpl.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>, |
|
[serverApi.IntegrationImpl.YoutubeWebScraper]: |
|
<Typography> |
|
This integration allows using the public Youtube Music search page to scrape |
|
for music metadata. <br /> |
|
Because it relies on reverse-engineering of a web page that may change in the |
|
future, this is considered to be experimental and unstable. However, the music links acquired |
|
using this method are expected to remain reasonably stable. |
|
</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.onChange({ |
|
...props.integration, |
|
name: e.target.value, |
|
})} |
|
/> |
|
</Box> |
|
{props.integration.type === serverApi.IntegrationImpl.SpotifyClientCredentials && |
|
<EditSpotifyClientCredentialsDetails |
|
clientId={'clientId' in props.integration.details && |
|
props.integration.details.clientId || ""} |
|
clientSecret={props.integration.secretDetails && 'clientSecret' in props.integration.secretDetails ? |
|
props.integration.secretDetails.clientSecret : |
|
(props.isNew ? "" : null)} |
|
editing={props.editing || false} |
|
onChangeClientId={(v: string) => props.onChange && props.onChange({ |
|
...props.integration, |
|
details: { |
|
...props.integration.details, |
|
clientId: v, |
|
} |
|
})} |
|
onChangeClientSecret={(v: string) => props.onChange && props.onChange({ |
|
...props.integration, |
|
secretDetails: { |
|
...props.integration.secretDetails, |
|
clientSecret: v, |
|
} |
|
})} |
|
/> |
|
} |
|
{props.flashMessage && props.flashMessage} |
|
</CardContent> |
|
<CardActions> |
|
{props.showEditButton && <IconButton |
|
onClick={props.onEdit} |
|
><EditIcon /></IconButton>} |
|
{props.showSubmitButton && <IconButton |
|
onClick={() => props.onSubmit && props.onSubmit(props.integration)} |
|
><CheckIcon /></IconButton>} |
|
{props.showDeleteButton && <IconButton |
|
onClick={props.onDelete} |
|
><DeleteIcon /></IconButton>} |
|
{props.showCancelButton && <IconButton |
|
onClick={props.onCancel} |
|
><ClearIcon /></IconButton>} |
|
{props.showTestButton && <Button |
|
onClick={props.onTest} |
|
>Test</Button>} |
|
</CardActions> |
|
</Card> |
|
} |
|
|
|
let EditorWithTest = (props: any) => { |
|
const [testFlashMessage, setTestFlashMessage] = |
|
React.useState<React.ReactFragment | undefined>(undefined); |
|
let { integration, ...rest } = props; |
|
return <EditIntegration |
|
onTest={() => { |
|
integration.integration.test({}) |
|
.then(() => { |
|
setTestFlashMessage( |
|
<Alert severity="success">Integration is active.</Alert> |
|
) |
|
}) |
|
}} |
|
flashMessage={testFlashMessage} |
|
showTestButton={true} |
|
integration={integration.properties} |
|
{...rest} |
|
/>; |
|
} |
|
|
|
function AddIntegrationMenu(props: { |
|
position: null | number[], |
|
open: boolean, |
|
onClose?: () => void, |
|
onAdd?: (type: serverApi.IntegrationImpl) => 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 && props.onAdd(serverApi.IntegrationImpl.SpotifyClientCredentials); |
|
props.onClose && props.onClose(); |
|
}} |
|
>Spotify via Client Credentials</MenuItem> |
|
<MenuItem |
|
onClick={() => { |
|
props.onAdd && props.onAdd(serverApi.IntegrationImpl.YoutubeWebScraper); |
|
props.onClose && props.onClose(); |
|
}} |
|
>Youtube Music Web Scraper</MenuItem> |
|
</Menu> |
|
} |
|
|
|
function EditIntegrationDialog(props: { |
|
open: boolean, |
|
onClose?: () => void, |
|
upstreamId?: number, |
|
integration: IntegrationState, |
|
onSubmit?: (p: serverApi.PostIntegrationRequest) => void, |
|
isNew: boolean, |
|
}) { |
|
let [editingIntegration, setEditingIntegration] = |
|
useState<IntegrationState>(props.integration); |
|
|
|
useEffect(() => { setEditingIntegration(props.integration); }, [props.integration]); |
|
|
|
return <Dialog |
|
onClose={props.onClose} |
|
open={props.open} |
|
disableBackdropClick={true} |
|
> |
|
<DialogTitle>Edit Integration</DialogTitle> |
|
<EditIntegration |
|
isNew={props.isNew} |
|
editing={true} |
|
upstreamId={props.upstreamId} |
|
integration={editingIntegration.properties} |
|
showCancelButton={true} |
|
showSubmitButton={props.onSubmit !== undefined} |
|
showTestButton={false} |
|
onCancel={props.onClose} |
|
onSubmit={props.onSubmit} |
|
onChange={(i: any) => { |
|
setEditingIntegration({ |
|
...editingIntegration, |
|
properties: i, |
|
integration: makeIntegration(i, editingIntegration.id), |
|
}); |
|
}} |
|
/> |
|
</Dialog> |
|
} |
|
|
|
export default function IntegrationSettings(props: {}) { |
|
const [addMenuPos, setAddMenuPos] = React.useState<null | number[]>(null); |
|
const [editingState, setEditingState] = React.useState<IntegrationState | null>(null); |
|
|
|
let { |
|
state: integrations, |
|
addIntegration, |
|
modifyIntegration, |
|
deleteIntegration, |
|
updateFromUpstream, |
|
} = useIntegrations(); |
|
|
|
const onOpenAddMenu = (e: any) => { |
|
setAddMenuPos([e.clientX, e.clientY]) |
|
}; |
|
const onCloseAddMenu = () => { |
|
setAddMenuPos(null); |
|
}; |
|
|
|
return <> |
|
<Box> |
|
{integrations === null && <CircularProgress />} |
|
{Array.isArray(integrations) && <Box display="flex" flexDirection="column" alignItems="center" flexWrap="wrap"> |
|
{integrations.map((state: IntegrationState) => <Box m={1} width="90%"> |
|
<EditorWithTest |
|
upstreamId={state.id} |
|
integration={state} |
|
showEditButton={true} |
|
showDeleteButton={true} |
|
onEdit={() => { setEditingState(state); }} |
|
onDelete={() => { |
|
deleteIntegration(state.id) |
|
.then(updateFromUpstream) |
|
}} |
|
/> |
|
</Box>)} |
|
<IconButton onClick={onOpenAddMenu}> |
|
<AddIcon /> |
|
</IconButton> |
|
</Box>} |
|
</Box> |
|
<AddIntegrationMenu |
|
position={addMenuPos} |
|
open={addMenuPos !== null} |
|
onClose={onCloseAddMenu} |
|
onAdd={(type: serverApi.IntegrationImpl) => { |
|
let p = makeDefaultIntegrationProperties(type); |
|
setEditingState({ |
|
properties: p, |
|
integration: makeIntegration(p, -1), |
|
id: -1, |
|
}) |
|
}} |
|
/> |
|
{editingState && <EditIntegrationDialog |
|
open={!(editingState === null)} |
|
onClose={() => { setEditingState(null); }} |
|
integration={editingState} |
|
isNew={editingState.id === -1} |
|
onSubmit={(v: serverApi.PostIntegrationRequest) => { |
|
if (editingState.id >= 0) { |
|
const id = editingState.id; |
|
setEditingState(null); |
|
modifyIntegration(id, v) |
|
.then(updateFromUpstream) |
|
} else { |
|
setEditingState(null); |
|
createIntegration({ |
|
...v, |
|
secretDetails: v.secretDetails || {}, |
|
}) |
|
.then(updateFromUpstream) |
|
} |
|
}} |
|
/>} |
|
</>; |
|
} |