From 5ecd9a6e97378c0480555415356c70ffaf5988b5 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Wed, 31 May 2023 14:30:13 +0200 Subject: [PATCH 1/2] ui: Add interface to manage integrations --- internal/ui/handlers.go | 139 ++++++++++++++ internal/ui/routes.go | 7 + ui/src/App.jsx | 2 + ui/src/components/Navigation.js | 5 + ui/src/pages/Integrations/IntegrationModal.js | 172 ++++++++++++++++++ .../pages/Integrations/NewIntegrationModal.js | 153 ++++++++++++++++ ui/src/pages/Integrations/index.jsx | 116 ++++++++++++ ui/src/services/api.service.js | 30 +++ 8 files changed, 624 insertions(+) create mode 100644 ui/src/pages/Integrations/IntegrationModal.js create mode 100644 ui/src/pages/Integrations/NewIntegrationModal.js create mode 100644 ui/src/pages/Integrations/index.jsx diff --git a/internal/ui/handlers.go b/internal/ui/handlers.go index 7a32f406..e30e7823 100644 --- a/internal/ui/handlers.go +++ b/internal/ui/handlers.go @@ -20,6 +20,7 @@ const ( browserIDContextKey = "browserID" isSync15Key = "sync15" docIDParam = "docid" + intIDParam = "intid" uiLogger = "[ui] " ui10 = " [10] " useridParam = "userid" @@ -509,3 +510,141 @@ func (app *ReactAppWrapper) createUser(c *gin.Context) { } c.Status(http.StatusCreated) } + +func (app *ReactAppWrapper) listIntegrations(c *gin.Context) { + uid := c.GetString(userIDContextKey) + + user, err := app.userStorer.GetUser(uid) + if err != nil { + log.Error(err) + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + c.JSON(http.StatusOK, user.Integrations) +} + +func (app *ReactAppWrapper) createIntegration(c *gin.Context) { + int := model.IntegrationConfig{} + if err := c.ShouldBindJSON(&int); err != nil { + log.Error(err) + badReq(c, err.Error()) + return + } + + uid := c.GetString(userIDContextKey) + + user, err := app.userStorer.GetUser(uid) + if err != nil { + log.Error(err) + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + int.ID = uuid.NewString() + user.Integrations = append(user.Integrations, int) + + err = app.userStorer.UpdateUser(user) + + if err != nil { + log.Error("error updating user", err) + c.AbortWithStatus(http.StatusInternalServerError) + return + } + + c.JSON(http.StatusOK, int) +} + +func (app *ReactAppWrapper) getIntegration(c *gin.Context) { + uid := c.GetString(userIDContextKey) + + intid := common.ParamS(intIDParam, c) + + user, err := app.userStorer.GetUser(uid) + if err != nil { + log.Error(err) + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + for _, integration := range user.Integrations { + if integration.ID == intid { + c.JSON(http.StatusOK, integration) + return + } + } + + c.AbortWithStatus(http.StatusNotFound) +} + +func (app *ReactAppWrapper) updateIntegration(c *gin.Context) { + int := model.IntegrationConfig{} + if err := c.ShouldBindJSON(&int); err != nil { + log.Error(err) + badReq(c, err.Error()) + return + } + + uid := c.GetString(userIDContextKey) + + intid := common.ParamS(intIDParam, c) + + user, err := app.userStorer.GetUser(uid) + if err != nil { + log.Error(err) + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + for idx, integration := range user.Integrations { + if integration.ID == intid { + int.ID = integration.ID + user.Integrations[idx] = int + + err = app.userStorer.UpdateUser(user) + + if err != nil { + log.Error("error updating user", err) + c.AbortWithStatus(http.StatusInternalServerError) + return + } + + c.JSON(http.StatusOK, int) + return + } + } + + c.AbortWithStatus(http.StatusNotFound) +} + +func (app *ReactAppWrapper) deleteIntegration(c *gin.Context) { + uid := c.GetString(userIDContextKey) + + intid := common.ParamS(intIDParam, c) + + user, err := app.userStorer.GetUser(uid) + if err != nil { + log.Error(err) + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + for idx, integration := range user.Integrations { + if integration.ID == intid { + user.Integrations = append(user.Integrations[:idx], user.Integrations[idx+1:]...) + + err = app.userStorer.UpdateUser(user) + + if err != nil { + log.Error("error updating user", err) + c.AbortWithStatus(http.StatusInternalServerError) + return + } + + c.Status(http.StatusAccepted) + return + } + } + + c.AbortWithStatus(http.StatusNotFound) +} diff --git a/internal/ui/routes.go b/internal/ui/routes.go index 718efaaa..4df7fd14 100644 --- a/internal/ui/routes.go +++ b/internal/ui/routes.go @@ -72,6 +72,13 @@ func (app *ReactAppWrapper) RegisterRoutes(router *gin.Engine) { auth.POST("folders", app.createFolder) auth.GET("documents/:docid/metadata", app.getDocumentMetadata) + // integrations + auth.GET("integrations", app.listIntegrations) + auth.POST("integrations", app.createIntegration) + auth.GET("integrations/:intid", app.getIntegration) + auth.PUT("integrations/:intid", app.updateIntegration) + auth.DELETE("integrations/:intid", app.deleteIntegration) + //admin admin := auth.Group("") admin.Use(app.adminMiddleware()) diff --git a/ui/src/App.jsx b/ui/src/App.jsx index 67fe8997..e764c669 100644 --- a/ui/src/App.jsx +++ b/ui/src/App.jsx @@ -12,6 +12,7 @@ import Login from "./pages/Login"; import Home from "./pages/Home"; import Connect from "./pages/Connect"; import Documents from "./pages/Documents"; +import Integrations from "./pages/Integrations"; import Profile from "./pages/Profile"; import Admin from "./pages/Admin"; import NoMatch from "./pages/404"; @@ -35,6 +36,7 @@ export default function App() { + diff --git a/ui/src/components/Navigation.js b/ui/src/components/Navigation.js index cc1c4aa4..4607efc5 100644 --- a/ui/src/components/Navigation.js +++ b/ui/src/components/Navigation.js @@ -33,6 +33,11 @@ const NavigationBar = () => { Documents + + + Integrations + + Connect diff --git a/ui/src/pages/Integrations/IntegrationModal.js b/ui/src/pages/Integrations/IntegrationModal.js new file mode 100644 index 00000000..a2f9d156 --- /dev/null +++ b/ui/src/pages/Integrations/IntegrationModal.js @@ -0,0 +1,172 @@ +import React, { useState } from "react"; +import Form from "react-bootstrap/Form"; +import { Button, Card } from "react-bootstrap"; +import apiService from "../../services/api.service"; + +import { Alert } from "react-bootstrap"; + +export default function IntegrationModal(params) { + const { integration, onSave, headerText, onClose } = params; + + const [formErrors, setFormErrors] = useState({}); + const [integrationForm, setIntegrationForm] = useState({ + name: integration?.Name, + provider: integration?.Provider, + email: integration?.email, + username: integration?.Username, + password: integration?.Password, + address: integration?.Address, + insecure: integration?.Insecure, + accesstoken: integration?.Accesstoken, + path: integration?.Path, + }); + + function handleChange({ target }) { + setIntegrationForm({ ...integrationForm, [target.name]: target.value }); + } + + function formIsValid() { + const _errors = {}; + + if (!integrationForm.name) _errors.error = "name is required"; + + setFormErrors(_errors); + + return Object.keys(_errors).length === 0; + } + + async function handleSubmit(event) { + event.preventDefault(); + + if (!formIsValid()) return; + + try { + await apiService.updateintegration({ + id: integration.ID, + name: integrationForm.name, + provider: integrationForm.provider, + username: integrationForm.username, + password: integrationForm.password, + address: integrationForm.address, + insecure: integrationForm.insecure, + accesstoken: integrationForm.accesstoken, + path: integrationForm.path, + }); + onSave(); + } catch (e) { + setFormErrors({ error: e.toString() }); + } + } + + if (!integration) return null; + return ( +
+ + + {headerText} + + +
+ + + IntegrationID + + + Provider + + + + + + + Name + + + {integrationForm.provider === "webdav" && ( + <> + Address + + + )} + {integrationForm.provider === "webdav" && ( + <> + Username + + + )} + {integrationForm.provider === "webdav" && ( + <> + Password + + + )} + + {integrationForm.provider === "localfs" && ( + <> + Path + + + )} + + {integrationForm.provider === "dropbox" && ( + <> + Access Token + + + )} +
+
+ + + + +
+
+ ); +} diff --git a/ui/src/pages/Integrations/NewIntegrationModal.js b/ui/src/pages/Integrations/NewIntegrationModal.js new file mode 100644 index 00000000..7490f37b --- /dev/null +++ b/ui/src/pages/Integrations/NewIntegrationModal.js @@ -0,0 +1,153 @@ +import React, { useState } from "react"; +import Form from "react-bootstrap/Form"; +import { Button, Card } from "react-bootstrap"; +import apiService from "../../services/api.service"; + +import { Alert } from "react-bootstrap"; + +export default function IntegrationProfileModal(params) { + const { onSave, onClose } = params; + + const [formErrors, setFormErrors] = useState({}); + const [formInfo, setFormInfo] = useState({}); + const [integrationForm, setIntegrationForm] = useState({ + name: "", + provider: "localfs", + }); + + function handleChange({ target }) { + setIntegrationForm({ ...integrationForm, [target.name]: target.value }); + } + + function formIsValid() { + const _errors = {}; + + if (!integrationForm.name) _errors.error = "name is required"; + + if (!integrationForm.provider) _errors.error = "provider is required"; + + setFormErrors(_errors); + + return Object.keys(_errors).length === 0; + } + + async function handleSubmit(event) { + event.preventDefault(); + + if (!formIsValid()) return; + + try { + await apiService.createintegration(integrationForm); + setFormInfo({ message: "Created" }); + onSave(); + } catch (e) { + setFormErrors({ error: e.toString() }); + } + } + + return ( +
+ + + New Integration + + + + + + + Name + + + Provider + + + + + + + {integrationForm.provider === "webdav" && ( + <> + Address + + + )} + {integrationForm.provider === "webdav" && ( + <> + Username + + + )} + {integrationForm.provider === "webdav" && ( + <> + Password + + + )} + + {integrationForm.provider === "localfs" && ( + <> + Path + + + )} + + {integrationForm.provider === "dropbox" && ( + <> + Access Token + + + )} + + + + + + +
+ ); +} diff --git a/ui/src/pages/Integrations/index.jsx b/ui/src/pages/Integrations/index.jsx new file mode 100644 index 00000000..1c94cc75 --- /dev/null +++ b/ui/src/pages/Integrations/index.jsx @@ -0,0 +1,116 @@ +import React, {useState} from "react"; +import useFetch from "../../hooks/useFetch"; +import Spinner from "../../components/Spinner"; +import {Alert, Button, Card, Container, Modal, Table} from "react-bootstrap"; +import IntegrationModal from "./IntegrationModal"; +import NewIntegrationModal from "./NewIntegrationModal"; +import apiService from "../../services/api.service"; +import { toast } from "react-toastify"; +const integrationListUrl = "integrations"; + +const NewIntegration = 1; +const UpdateIntegration = 2; +const Integrations = () => { + const [index, setIndex] = useState(0); + const { data: integrationList, error, loading } = useFetch(`${integrationListUrl}`, index); + const [ state, setState ] = useState({showModal: 0, modalIntegration: null}); + const refresh = () =>{ + setIndex(previous => previous+1) + } + + function openModal(index: number) { + if (!integrationList) return; + let integration = integrationList[index]; + setState({ + showModal: UpdateIntegration, + modalIntegration: integration, + }); + } + function closeModal() { + setState({ + showModal: 0, + modalIntegration: null, + }); + } + + if (loading) { + return + } + + if (error) { + return ( + + An Error Occurred + {`Error ${error.status}: ${error.statusText}`} + + ); + } + + const newIntegration = e => { + setState({ + showModal: NewIntegration, + }); + } + + const onSave = () => { + closeModal(); + refresh(); + } + + const remove = async (e, id, name) => { + e.preventDefault() + e.stopPropagation() + if (!window.confirm(`Are you sure you want to delete integration: ${name}?`)) + return false + + try{ + await apiService.deleteintegration(id) + refresh() + } catch(e){ + toast.error('Error:'+ e) + } + } + + return ( + +

Integrations

+ + + + + + + + + + + + + {!integrationList.length && ( + + + + )} + {integrationList.map((i, index) => ( + openModal(index)} style={{ cursor: "pointer" }}> + + + + + + + ))} + +
#IntegrationIdNameProvider
No integration
{index}{i.ID}{i.Name}{i.Provider}
+ + + + + + +
+
+ ); +}; + +export default Integrations; diff --git a/ui/src/services/api.service.js b/ui/src/services/api.service.js index 7fde7925..a8a07ef5 100644 --- a/ui/src/services/api.service.js +++ b/ui/src/services/api.service.js @@ -127,6 +127,36 @@ class ApiServices { headers: this.header(), }).then((r) => handleError(r)); } + + listintegration() { + return fetch(`${constants.ROOT_URL}/integrations`, { + method: "GET", + headers: this.header(), + }).then((r) => { + handleError(r); + return r.json(); + }); + } + updateintegration(integration) { + return fetch(`${constants.ROOT_URL}/integrations/${integration.id}`, { + method: "PUT", + headers: this.header(), + body: JSON.stringify(integration), + }).then((r) => handleError(r)); + } + createintegration(integration) { + return fetch(`${constants.ROOT_URL}/integrations`, { + method: "POST", + headers: this.header(), + body: JSON.stringify(integration), + }).then((r) => handleError(r)); + } + deleteintegration(integrationid) { + return fetch(`${constants.ROOT_URL}/integrations/${integrationid}`, { + method: "DELETE", + headers: this.header(), + }).then((r) => handleError(r)); + } } function removeUser(){ From 2937e8eacb102435873fcc3cd45672b92628de39 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Wed, 31 May 2023 14:34:15 +0200 Subject: [PATCH 2/2] Reorder providers --- internal/integrations/integrations.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/integrations/integrations.go b/internal/integrations/integrations.go index a7f4986c..b4f59dc5 100644 --- a/internal/integrations/integrations.go +++ b/internal/integrations/integrations.go @@ -37,12 +37,12 @@ func GetIntegrationProvider(storer storage.UserStorer, uid, integrationid string continue } switch intg.Provider { - case webdavProvider: - return newWebDav(intg), nil case dropboxProvider: return newDropbox(intg), nil case localfsProvider: return newLocalFS(intg), nil + case webdavProvider: + return newWebDav(intg), nil } } return nil, fmt.Errorf("integration not found or no implmentation (only webdav) %s", integrationid)