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 props.onChangeClientId(e.target.value)} /> { props.onChangeClientSecret(e.target.value) }} onFocus={(e: any) => { if (props.clientSecret === null) { // Change from dots to empty input console.log("Focus!") props.onChangeClientSecret(''); } }} /> ; } // 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 = { [serverApi.IntegrationImpl.SpotifyClientCredentials]: {new IntegrationClasses[serverApi.IntegrationImpl.SpotifyClientCredentials](-1).getIcon({ style: { height: '40px', width: '40px' } })} Spotify (using Client Credentials) , [serverApi.IntegrationImpl.YoutubeWebScraper]: {new IntegrationClasses[serverApi.IntegrationImpl.YoutubeWebScraper](-1).getIcon({ style: { height: '40px', width: '40px' } })} Youtube Music (using experimental web scraper) , } let IntegrationDescription: Record = { [serverApi.IntegrationImpl.SpotifyClientCredentials]: 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.
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.
, [serverApi.IntegrationImpl.YoutubeWebScraper]: This integration allows using the public Youtube Music search page to scrape for music metadata.
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.
, } return {IntegrationDescription[props.integration.type]} props.onChange && props.onChange({ ...props.integration, name: e.target.value, })} /> {props.integration.type === serverApi.IntegrationImpl.SpotifyClientCredentials && 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} {props.showEditButton && } {props.showSubmitButton && props.onSubmit && props.onSubmit(props.integration)} >} {props.showDeleteButton && } {props.showCancelButton && } {props.showTestButton && } } let EditorWithTest = (props: any) => { const [testFlashMessage, setTestFlashMessage] = React.useState(undefined); let { integration, ...rest } = props; return { integration.integration.test({}) .then(() => { setTestFlashMessage( Integration is active. ) }) }} 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 { props.onAdd && props.onAdd(serverApi.IntegrationImpl.SpotifyClientCredentials); props.onClose && props.onClose(); }} >Spotify via Client Credentials { props.onAdd && props.onAdd(serverApi.IntegrationImpl.YoutubeWebScraper); props.onClose && props.onClose(); }} >Youtube Music Web Scraper } function EditIntegrationDialog(props: { open: boolean, onClose?: () => void, upstreamId?: number, integration: IntegrationState, onSubmit?: (p: serverApi.PostIntegrationRequest) => void, isNew: boolean, }) { let [editingIntegration, setEditingIntegration] = useState(props.integration); useEffect(() => { setEditingIntegration(props.integration); }, [props.integration]); return Edit Integration { setEditingIntegration({ ...editingIntegration, properties: i, integration: makeIntegration(i, editingIntegration.id), }); }} /> } export default function IntegrationSettings(props: {}) { const [addMenuPos, setAddMenuPos] = React.useState(null); const [editingState, setEditingState] = React.useState(null); let { state: integrations, addIntegration, modifyIntegration, deleteIntegration, updateFromUpstream, } = useIntegrations(); const onOpenAddMenu = (e: any) => { setAddMenuPos([e.clientX, e.clientY]) }; const onCloseAddMenu = () => { setAddMenuPos(null); }; return <> {integrations === null && } {Array.isArray(integrations) && {integrations.map((state: IntegrationState) => { setEditingState(state); }} onDelete={() => { deleteIntegration(state.id) .then(updateFromUpstream) }} /> )} } { let p = makeDefaultIntegrationProperties(type); setEditingState({ properties: p, integration: makeIntegration(p, -1), id: -1, }) }} /> {editingState && { 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) } }} />} ; }