diff --git a/.env b/.env new file mode 100644 index 0000000..ede85ab --- /dev/null +++ b/.env @@ -0,0 +1,4 @@ +REACT_APP_TM_SERVER_SCHEME=https +REACT_APP_TM_SERVER_HOST=api.emporio.vaimee.it +REACT_APP_TM_SERVER_PORT=80 +REACT_APP_REMOTE_SERVER=false \ No newline at end of file diff --git a/src/assets/main.css b/src/assets/main.css index 7212f3f..450c513 100644 --- a/src/assets/main.css +++ b/src/assets/main.css @@ -335,7 +335,7 @@ ul { * to override it to ensure consistency even when using the default theme. */ -html { + html { font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; /* 1 */ line-height: 1.5; /* 2 */ } @@ -624,11 +624,20 @@ video { border-color: rgba(252, 129, 129, var(--tw-border-opacity)); } +.border-blue-500 { + --tw-border-opacity: 1; + border-color: rgba(0, 90, 156, var(--tw-border-opacity)); +} + .focus\:border-blue-500:focus { --tw-border-opacity: 1; border-color: rgba(0, 90, 156, var(--tw-border-opacity)); } +.border-none{ + border-style: none; +} + .rounded { border-radius: 0.25rem; } @@ -1129,6 +1138,14 @@ video { width: 33.333333%; } +.w-1\/4 { + width: 25%; +} + +.w-7\/10 { + width: 70%; +} + .w-5\/12 { width: 41.666667%; } @@ -1149,6 +1166,10 @@ video { z-index: 10; } +.whitespace-nowrap { + white-space: nowrap; +} + @keyframes spin { to { transform: rotate(360deg); diff --git a/src/components/App/AppHeader/AppHeader.jsx b/src/components/App/AppHeader/AppHeader.jsx index dbc2650..465eb80 100644 --- a/src/components/App/AppHeader/AppHeader.jsx +++ b/src/components/App/AppHeader/AppHeader.jsx @@ -20,6 +20,8 @@ import Button from "./Button"; import { ShareDialog } from "../../Dialogs/ShareDialog"; import { ConvertTmDialog } from "../../Dialogs/ConvertTmDialog"; import { CreateTdDialog } from "../../Dialogs/CreateTdDialog"; +import { LoadTmDialog } from "../../Dialogs/LoadTmDialog"; +import { SaveTmRemotelyDialog } from "../../Dialogs/SaveTmRemotelyDialog"; import { getFileHandle, getFileHTML5, _readFileHTML5 } from "../../../util.js"; @@ -306,6 +308,15 @@ export default function AppHeader() { const createTdDialog = React.useRef(); const openCreateTdDialog = () => { createTdDialog.current.openModal() } + const loadTmDialog = React.useRef(); + const openLoadTmDialog = () => { loadTmDialog.current.openModal() } + + const saveTmRemotelyDialog = React.useRef(); + const openSaveTmRemotelyDialog = () => { saveTmRemotelyDialog.current.openModal() } + + const useRemoteServer = process.env.REACT_APP_REMOTE_SERVER.toLocaleLowerCase() === "true"; + console.log(useRemoteServer); + console.log(useRemoteServer? 'true':'false'); return ( <> @@ -320,14 +331,18 @@ export default function AppHeader() { Share New Open + {useRemoteServer && Load TM} {(hasNativeFS()) && Save} Save As + {(context.isThingModel && useRemoteServer) && Save Remotely} {(context.showConvertBtn || context.isThingModel) && Convert To TD} + + { context.updateLinkedTd(undefined) context.addLinkedTd(linkedTd) context.updateShowConvertBtn(type === "TM"); + context.updateIsThingModel(type === "TM") context.updateOfflineTD(JSON.stringify(td, null, "\t"),"AppHeader"); close(); }} diff --git a/src/components/Dialogs/LoadTmDialog.jsx b/src/components/Dialogs/LoadTmDialog.jsx new file mode 100644 index 0000000..9559f82 --- /dev/null +++ b/src/components/Dialogs/LoadTmDialog.jsx @@ -0,0 +1,402 @@ +/******************************************************************************** + * Copyright (c) 2018 - 2021 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and + * + * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 + ********************************************************************************/ +import React, { + forwardRef, + useImperativeHandle, + useContext, + useReducer, +} from "react"; +import ReactDOM from "react-dom"; +import { DialogTemplate } from "./DialogTemplate"; +import { AdvancedOptions } from "./components/AdvancedOptions" +import ediTDorContext from "../../context/ediTDorContext"; +import { + ChevronDown, + ChevronRight, + ChevronLeft, +} from "react-feather"; + +function tmReducer(state, action) { + switch (action.type) { + case "thingModels": { + return { + ...state, + thingModels: action.payload.map((thingModel) => { + return { thingModel: thingModel, select: false }; + }), + }; + } + case "selected": { + const index = action.payload; + let choosenModel; + const thingModels = state.thingModels.map( + (thingObject, thingIndex) => { + if (thingIndex === index) { + thingObject.selected = true; + choosenModel = thingObject.thingModel; + } else thingObject.selected = false; + return thingObject; + } + ); + return { + ...state, + thingModels: thingModels, + choosenModel: choosenModel, + }; + } + case "changePage": { + const pagination = { + ...state.pagination, + currentPage: action.payload, + }; + return { + ...state, + pagination: pagination, + }; + } + case "reset": { + const pagination = { + ...state.pagination, + currentPage: 0, + }; + return { + ...state, + pagination: pagination, + }; + } + case "field": { + return { + ...state, + [action.fieldName]: action.payload, + }; + } + default: + throw Error( + "Unexected reducer case in Thing Model Modal" + ); + } +} + +const initialState = { + thingModels: [], + choosenModel: null, + pagination: { + currentPage: 0, + thingModelsPerPage: 5, + }, +}; + +export const LoadTmDialog = forwardRef((props, ref) => { + const context = useContext(ediTDorContext); + + const [display, setDisplay] = React.useState(() => { + return false; + }); + const [state, dispatch] = useReducer( + tmReducer, + initialState + ); + + const { + thingModels, + choosenModel, + pagination, + } = state; + + useImperativeHandle(ref, () => { + return { + openModal: () => open(), + close: () => close(), + }; + }); + + const open = async () => { + dispatch({ + type: "thingModels", + payload: await fetchThingModels(), + }); + dispatch({ type: "reset", payload: 0 }); + setDisplay(true); + }; + + const close = () => { + setDisplay(false); + }; + + const setSelectedThingModel = (index) => { + dispatch({ type: "selected", payload: index }); + }; + + const fetchThingModels = async ({ + page = 0, + attribute = false, + searchText, + remoteUrl = context.tmRepositoryUrl, + } = {}) => { + const offset = pagination.thingModelsPerPage * page; + let url = `${remoteUrl}/models?limit=${pagination.thingModelsPerPage}&offset=${offset}`; + if (attribute) url += `&${attribute}=${searchText}`; + const res = await fetch(url); + const data = await res.json(); + return data; + }; + + const paginate = async (direction) => { + const page = + direction === "right" + ? pagination.currentPage + 1 + : pagination.currentPage - 1; + if (page < 0) return; + + const searchText = + document.getElementById("search-id").value; + const attribute = + document.getElementById("search-option").value; + + const thingModels = + searchText === "" + ? await fetchThingModels({ page: page }) + : await fetchThingModels({ + page: page, + attribute: attribute, + searchText: searchText, + }); + if (thingModels.length <= 0) return; + + dispatch({ + type: "thingModels", + payload: thingModels, + }); + dispatch({ type: "changePage", payload: page }); + }; + + const changeThingModelUrl = async () => { + const url = document.getElementById("remote-url").value; + try { + const thingModels = await fetchThingModels({ + remoteUrl: url, + }); + context.updateTmRepositoryUrl(url); + + return dispatch({ + type: "thingModels", + payload: thingModels, + }); + } catch (error) { + const msg = `Error processing URL - Thing Model Repository was not found`; + alert(msg); + } + }; + const searchThingModels = async () => { + const searchText = + document.getElementById("search-id").value; + const attribute = + document.getElementById("search-option").value; + + //TODO: is that right? + dispatch({ type: "reset" }); + + const thingModels = + searchText === "" + ? await fetchThingModels() + : await fetchThingModels({ + page: 0, + attribute: attribute, + searchText: searchText, + }); + return dispatch({ + type: "thingModels", + payload: thingModels, + }); + }; + + const content = buildForm( + thingModels, + pagination.currentPage, + setSelectedThingModel, + searchThingModels, + paginate, + changeThingModelUrl + ); + + if (display) { + return ReactDOM.createPortal( + { + if (choosenModel === null) return; + let linkedModel = {}; + linkedModel[choosenModel["title"]] = choosenModel; + context.updateLinkedTd(undefined); + context.addLinkedTd(linkedModel); + context.updateShowConvertBtn(true); + context.updateIsThingModel(true); + context.updateOfflineTD( + JSON.stringify(choosenModel, null, "\t"), + "AppHeader" + ); + close(); + }} + children={content} + submitText={"Load TM"} + title={"Load new TM"} + description={ + "Choose a template Thing Model to load" + } + />, + document.getElementById("modal-root") + ); + } + + return null; +}); + +const buildForm = ( + thingModelObjects, + page, + setSelectedThingModel, + searchThingModels, + paginate, + changeUrl +) => { + return ( + <> + + + + + {thingModelObjects.map((thingModelObject, index) => ( + + ))} + + + > + ); +}; + +const SearchBar = ({ searchThingModels }) => { + return ( + + + + title + description + type + + + + + + + + { + searchThingModels(); + }} + > + Search + + + ); +}; + +const Pagination = ({ paginate, page }) => { + return ( + + paginate("left")} + > + + + + {" "} + {page + 1}{" "} + + paginate("right")} + > + + + + ); +}; + +const ThingModel = ({ + thingModelObject, + index, + setSelectedThingModel, +}) => { + const types = formatThingModeltypes( + thingModelObject.thingModel["@type"] + ); + return ( + { + setSelectedThingModel(index); + }} + > + + + {thingModelObject.thingModel.title} + + + {types ? types.map((type) => {type}) : ""} + + + + {thingModelObject.thingModel.description} + + + ); +}; + +const formatThingModeltypes = (type) => { + if (Array.isArray(type)) { + return type.filter( + (element) => element !== "tm:ThingModel" + ); + } +}; diff --git a/src/components/Dialogs/SaveTmRemotelyDialog.jsx b/src/components/Dialogs/SaveTmRemotelyDialog.jsx new file mode 100644 index 0000000..50fcdad --- /dev/null +++ b/src/components/Dialogs/SaveTmRemotelyDialog.jsx @@ -0,0 +1,184 @@ +/******************************************************************************** + * Copyright (c) 2018 - 2021 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and + * + * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 + ********************************************************************************/ +import React, { + forwardRef, + useContext, + useEffect, + useImperativeHandle, +} from "react"; +import ReactDOM from "react-dom"; +import ediTDorContext from "../../context/ediTDorContext"; +import { AdvancedOptions } from "./components/AdvancedOptions" +import { DialogTemplate } from "./DialogTemplate"; + +export const SaveTmRemotelyDialog = forwardRef((props, ref) => { + const context = useContext(ediTDorContext); + const [display, setDisplay] = React.useState(() => { + return false; + }); + + useEffect(() => { + if (display === true) { + } + }, [display, context]); + + useImperativeHandle(ref, () => { + return { + openModal: () => open(), + close: () => close(), + }; + }); + + const open = () => { + setDisplay(true); + }; + + const close = () => { + setDisplay(false); + }; + + const checkForDuplicates = async ( + thingModel, + credential + ) => { + const response = await performPostRequest( + thingModel, + credential, + "models/is-duplicate" + ); + if (!response.ok) return handleError(response); + + const isDuplicateString = await response.text(); + return isDuplicateString.toLowerCase() === "true"; + }; + + const saveTm = async (thingModel, credential) => { + const isDuplicate = await checkForDuplicates( + thingModel, + credential + ); + let confirmation = false; + if (isDuplicate) { + const msg = + "The Thing Model send already exist in the give repository. Do you want to save it regardless?"; + confirmation = window.confirm(msg); + } + if (!isDuplicate || confirmation) { + const response = await performPostRequest( + thingModel, + credential + ); + if (!response.ok) handleError(response); + } + }; + + const handleError = (response) => { + let msg; + switch (response.status) { + case 401: + msg = "Invalid credentials provided"; + break; + case 500: + msg = + "Thing model repository is having troubles processing your request"; + break; + case 400: + msg = "Invalid thing model provided"; + break; + default: + msg = `We ran into an error trying to save your TD.`; + } + return alert(msg); + }; + + const performPostRequest = async ( + thingModel, + credential, + path = "models" + ) => { + return await fetch( + `${context.tmRepositoryUrl}/${path}`, + { + method: "POST", + body: thingModel, + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${credential}`, + }, + } + ); + }; + const changeUrl = async () =>{ + const url = document.getElementById("remote-url").value; + try { + //* is this the best way to check if this url is a valid thing model repository? + await fetch(`${url}/models?limit=1`) + context.updateTmRepositoryUrl(url); + } catch (error) { + const msg = `Error processing URL - Thing Model Repository was not found`; + alert(msg); + } + + + } + const urlField = createCredentialField(changeUrl); + + if (display) { + return ReactDOM.createPortal( + { + const credential = document.getElementById( + "credential-field" + ).value; + if (credential === null) return; + const thingModel = context.offlineTD; + saveTm(thingModel, credential); + close(); + }} + children={urlField} + title={"Create new TM"} + description={ + "Create a new TM in the remote Thing Model Repository" + } + />, + document.getElementById("modal-root") + ); + } + + return null; + } +); + +const createCredentialField = (changeUrl) => { + return ( + + + + Credential: + + + + ); +}; diff --git a/src/components/Dialogs/components/AdvancedOptions.jsx b/src/components/Dialogs/components/AdvancedOptions.jsx new file mode 100644 index 0000000..35e38d3 --- /dev/null +++ b/src/components/Dialogs/components/AdvancedOptions.jsx @@ -0,0 +1,57 @@ +import { ChevronDown, ChevronRight } from "react-feather"; +import ediTDorContext from "../../../context/ediTDorContext"; +import React from "react"; + +export const AdvancedOptions = ({ changeUrl }) => { + const context = React.useContext(ediTDorContext); + const [showUrl, setShowUrl] = React.useState(false); + const show = () => { + setShowUrl(!showUrl); + }; + return ( + <> + show()} + > + + + Advanced Options + + + {showUrl === true ? ( + + ) : ( + + )} + + + + {showUrl && ( + + + TM Repository: + + + + { + changeUrl(); + }} + > + Change + + + + )} + > + ); +}; diff --git a/src/context/GlobalState.js b/src/context/GlobalState.js index 3b11583..197898c 100644 --- a/src/context/GlobalState.js +++ b/src/context/GlobalState.js @@ -13,11 +13,15 @@ import React, { useReducer } from 'react'; import EdiTDorContext from './ediTDorContext'; -import { editdorReducer, REMOVE_FORM_FROM_TD, REMOVE_LINK_FROM_TD, UPDATE_IS_THINGMODEL, SET_FILE_HANDLE, UPDATE_IS_MODFIED, UPDATE_OFFLINE_TD, ADD_PROPERTYFORM_TO_TD, ADD_ACTIONFORM_TO_TD, ADD_EVENTFORM_TO_TD, REMOVE_ONE_OF_A_KIND_FROM_TD, UPDATE_SHOW_CONVERT_BTN, ADD_LINKED_TD, UPDATE_LINKED_TD} from './editorReducers'; +import { editdorReducer, REMOVE_FORM_FROM_TD, REMOVE_LINK_FROM_TD, UPDATE_IS_THINGMODEL, SET_FILE_HANDLE, UPDATE_IS_MODFIED, UPDATE_OFFLINE_TD, ADD_PROPERTYFORM_TO_TD, ADD_ACTIONFORM_TO_TD, ADD_EVENTFORM_TO_TD, REMOVE_ONE_OF_A_KIND_FROM_TD, UPDATE_SHOW_CONVERT_BTN,UPDATE_TM_REPOSITORY_URL, ADD_LINKED_TD, UPDATE_LINKED_TD} from './editorReducers'; const GlobalState = props => { - const [editdorState, dispatch] = useReducer(editdorReducer, { offlineTD: '', theme: 'dark' }); + const [editdorState, dispatch] = useReducer(editdorReducer, { + offlineTD: '', + theme: 'dark', + tmRepositoryUrl: `${process.env.REACT_APP_TM_SERVER_SCHEME}://${process.env.REACT_APP_TM_SERVER_HOST}:${process.env.REACT_APP_TM_SERVER_PORT}` + }); const updateOfflineTD = (offlineTD, props) => { dispatch({ type: UPDATE_OFFLINE_TD, offlineTD: offlineTD }); @@ -59,6 +63,9 @@ const GlobalState = props => { const updateShowConvertBtn = showConvertBtn => { dispatch({ type: UPDATE_SHOW_CONVERT_BTN, showConvertBtn: showConvertBtn }); }; + const updateTmRepositoryUrl = tmRepositoryUrl => { + dispatch({ type: UPDATE_TM_REPOSITORY_URL, tmRepositoryUrl: tmRepositoryUrl }); + }; const addLinkedTd = linkedTd => { dispatch({ type: ADD_LINKED_TD, linkedTd: linkedTd }); @@ -79,6 +86,7 @@ const GlobalState = props => { fileHandle: editdorState.fileHandle, showConvertBtn: editdorState.showConvertBtn, linkedTd: editdorState.linkedTd, + tmRepositoryUrl: editdorState.tmRepositoryUrl, updateOfflineTD, updateIsModified, updateIsThingModel, @@ -91,7 +99,8 @@ const GlobalState = props => { removeOneOfAKindReducer, updateShowConvertBtn, addLinkedTd, - updateLinkedTd + updateLinkedTd, + updateTmRepositoryUrl, }} > {props.children} diff --git a/src/context/ediTDorContext.js b/src/context/ediTDorContext.js index 17e4745..147b0c2 100644 --- a/src/context/ediTDorContext.js +++ b/src/context/ediTDorContext.js @@ -1,37 +1,15 @@ /******************************************************************************** * Copyright (c) 2018 - 2020 Contributors to the Eclipse Foundation - * + * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. - * + * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at * http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and - * + * * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 ********************************************************************************/ -import React from 'react'; +import React from "react"; -export default React.createContext({ - offlineTD: '', - theme: 'dark', - isModified: false, - isThingModel: undefined, - name: '', - fileHandle:'', - showConvertBtn: false, - linkedTd:{}, - updateOfflineTD: td => {}, - updateIsModified: isModified => {}, - updateIsThingModel: isThingModel => {}, - setFileHandle: handle => {}, - removeForm: form => {}, - addForm: form => {}, - removeLink: link => {}, - addActionForm: params => {}, - addEventForm: params => {}, - removeOneOfAKindReducer: (kind, oneOfAKind) => {}, - updateShowConvertBtn: showConvertBtn => {}, - addLinkedTd: linkedTd => {}, - updateLinkedTd: linkedTd => {} -}); \ No newline at end of file +export default React.createContext({}); \ No newline at end of file diff --git a/src/context/editorReducers.js b/src/context/editorReducers.js index 029aefa..541e3ba 100644 --- a/src/context/editorReducers.js +++ b/src/context/editorReducers.js @@ -21,6 +21,7 @@ export const ADD_ACTIONFORM_TO_TD = 'ADD_ACTIONFORM_TO_TD'; export const ADD_EVENTFORM_TO_TD = 'ADD_EVENTFORM_TO_TD'; export const REMOVE_ONE_OF_A_KIND_FROM_TD = 'REMOVE_ONE_OF_A_KIND_FROM_TD'; export const UPDATE_SHOW_CONVERT_BTN = 'UPDATE_SHOW_CONVERT_BTN'; +export const UPDATE_TM_REPOSITORY_URL = 'UPDATE_TM_REPOSITORY_URL'; export const ADD_LINKED_TD = 'ADD_LINKED_TD'; export const UPDATE_LINKED_TD = 'UPDATE_LINKED_TD' @@ -188,6 +189,11 @@ const updateShowConvertBtn = (showConvertBtn, state) => { return { ...state, showConvertBtn: showConvertBtn }; }; +const updateTmRepositoryUrl = (tmRepositoryUrl, state) => { + console.log('updateTmRepositoryUrl') + return { ...state, tmRepositoryUrl: tmRepositoryUrl }; +}; + const editdorReducer = (state, action) => { switch (action.type) { case UPDATE_OFFLINE_TD: @@ -213,6 +219,8 @@ const editdorReducer = (state, action) => { return addEventFormReducer(action.params, state) case UPDATE_SHOW_CONVERT_BTN: return updateShowConvertBtn(action.showConvertBtn, state); + case UPDATE_TM_REPOSITORY_URL: + return updateTmRepositoryUrl(action.tmRepositoryUrl, state); case ADD_LINKED_TD: return addLinkedTd(action.linkedTd,state) case UPDATE_LINKED_TD:
{type}
+ {thingModelObject.thingModel.description} +