From 4f410455f297716f15727f562dd5a5b8812f86a2 Mon Sep 17 00:00:00 2001 From: freshteapot Date: Thu, 14 Oct 2021 03:01:14 +0200 Subject: [PATCH 1/6] Extra tab to show files UX --- Source/SelfService/Web/api/staticFiles.ts | 46 ++++ .../Web/microservice/staticSite/files.tsx | 136 ++++++++++++ .../Web/microservice/staticSite/view.tsx | 206 ++++++++++++++++++ Source/SelfService/Web/microservice/view.tsx | 21 +- 4 files changed, 403 insertions(+), 6 deletions(-) create mode 100644 Source/SelfService/Web/api/staticFiles.ts create mode 100644 Source/SelfService/Web/microservice/staticSite/files.tsx create mode 100644 Source/SelfService/Web/microservice/staticSite/view.tsx diff --git a/Source/SelfService/Web/api/staticFiles.ts b/Source/SelfService/Web/api/staticFiles.ts new file mode 100644 index 000000000..e54da6da5 --- /dev/null +++ b/Source/SelfService/Web/api/staticFiles.ts @@ -0,0 +1,46 @@ +// Copyright (c) Dolittle. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +import { getServerUrlPrefix } from './api'; + + +export type StaticFiles = { + files: string[] +}; + + +export async function getFiles(applicationId: string, environment: string, microserviceId: string): Promise { + const url = `${getServerUrlPrefix()}/application/${applicationId}/environment/${environment.toLowerCase()}/staticFiles/${microserviceId}/list`; + + const response = await fetch(url, { + method: 'GET', + mode: 'cors', + }); + + const body: any = await response.json() as StaticFiles; + return body; +} + +export async function addFile(applicationId: string, environment: string, microserviceId: string, fileName: string, file: File): Promise { + const url = `${getServerUrlPrefix()}/application/${applicationId}/environment/${environment.toLowerCase()}/staticFiles/${microserviceId}/add/${fileName}`; + const response = await fetch(url, { + method: 'POST', + mode: 'cors', + body: file, + }); + + const body: any = await response.json() as StaticFiles; + return body; +} + +// deleteFile based on the filename that can be found via getFiles +export async function deleteFile(applicationId: string, environment: string, microserviceId: string, fileName: string): Promise { + const url = `${getServerUrlPrefix()}/application/${applicationId}/environment/${environment.toLowerCase()}/staticFiles/${microserviceId}/remove/${fileName}`; + // TODO add file + const response = await fetch(url, { + method: 'DELETE', + mode: 'cors', + }); + + const body: any = await response.json() as StaticFiles; + return body; +} diff --git a/Source/SelfService/Web/microservice/staticSite/files.tsx b/Source/SelfService/Web/microservice/staticSite/files.tsx new file mode 100644 index 000000000..68f57178f --- /dev/null +++ b/Source/SelfService/Web/microservice/staticSite/files.tsx @@ -0,0 +1,136 @@ +// Copyright (c) Dolittle. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import React from 'react'; +import { makeStyles, createStyles, Theme } from '@material-ui/core/styles'; +import Button from '@material-ui/core/Button'; +import IconButton from '@material-ui/core/IconButton'; +import PhotoCamera from '@material-ui/icons/PhotoCamera'; +import { addFile } from '../../api/staticFiles'; + +import { Button as DolittleButton } from '../../theme/button'; +import { TextField } from '@material-ui/core'; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + '& > *': { + margin: theme.spacing(1), + }, + }, + input: { + display: 'none', + }, + }), +); + +// https://stackoverflow.com/questions/40589302/how-to-enable-file-upload-on-reacts-material-ui-simple-input + +interface FormProps { + onClick: React.MouseEventHandler; //(fileName:Blob) => Promise, // callback taking a string and then dispatching a store actions + onChange: React.ChangeEventHandler; + onNameChange: React.ChangeEventHandler; +} + +const UploadButton: React.FunctionComponent = (props) => { + const classes = useStyles(); + + const [fileName, setFileName] = React.useState(''); + return ( +
+ + ) => { + setFileName(event.target.value!); + props!.onNameChange(event); + }} + /> + + + + + + + + Upload + +
+ ); +}; + +type Props = { + applicationId: string + environment: string + microserviceId: string + files: string[] +}; + +export const Files: React.FunctionComponent = (props) => { + const _props = props!; + const applicationId = _props.applicationId; + const microserviceId = _props.microserviceId; + const environment = _props.environment; + + const [selectedFile, setSelectedFile] = React.useState(null); + const [fileName, setFileName] = React.useState(''); + + const handleCapture = ({ target }: any) => { + setSelectedFile(target.files[0]); + }; + + const handleFileNameChange = ({ target }: any) => { + setFileName(target.value!); + }; + + const handleSubmit = () => { + addFile(applicationId, environment, microserviceId, fileName, selectedFile! as File); + }; + + const listView = _props.files.map((fileName: string) => { + return (

{fileName}

); + }); + + return ( + <> +

TODO: Poormans fileview

+

TODO: Upload file

+ + +

TODO: list files

+ {listView} +

TODO: Remove file

+ + + + + ); +}; diff --git a/Source/SelfService/Web/microservice/staticSite/view.tsx b/Source/SelfService/Web/microservice/staticSite/view.tsx new file mode 100644 index 000000000..3ee30073d --- /dev/null +++ b/Source/SelfService/Web/microservice/staticSite/view.tsx @@ -0,0 +1,206 @@ +// Copyright (c) Dolittle. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +import React, { useState, useEffect } from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; +import { + Grid, + IconButton, + Typography, + Divider, + Box, +} from '@material-ui/core'; +import { TabPanel } from '../../utils/materialUi'; +import DeleteIcon from '@material-ui/icons/Delete'; +import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'; +import { useSnackbar } from 'notistack'; + +import { ConfigViewK8s } from '../base/configViewK8s'; +import { microservices } from '../../stores/microservice'; + +import { HttpResponsePodStatus } from '../../api/api'; +import { HealthStatus } from '../view/healthStatus'; +import { useReadable } from 'use-svelte-store'; + +import { Tab, Tabs } from '../../theme/tabs'; +import { DownloadButtons } from '../components/downloadButtons'; +import { getFiles } from '../../api/staticFiles'; +import { Files } from './files'; + + +type Props = { + applicationId: string + environment: string + microserviceId: string + podsData: HttpResponsePodStatus +}; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + deleteIcon: { + 'padding': 0, + 'marginRight': theme.spacing(1), + 'fill': 'white', + '& .MuiSvgIcon-root': { + color: 'white', + marginRight: theme.spacing(1), + }, + '& .MuiTypography-root': { + color: 'white', + textTransform: 'uppercase' + } + }, + editIcon: { + 'padding': 0, + 'marginRight': theme.spacing(1), + 'fill': '#6678F6', + '& .MuiSvgIcon-root': { + color: '#6678F6', + marginRight: theme.spacing(1), + }, + '& .MuiTypography-root': { + color: '#6678F6', + textTransform: 'uppercase' + } + }, + root: { + flexGrow: 1, + }, + paper: { + padding: theme.spacing(2), + textAlign: 'center', + }, + divider: { + backgroundColor: '#3B3D48' + } + }) +); + +export const View: React.FunctionComponent = (props) => { + const { enqueueSnackbar } = useSnackbar(); + const classes = useStyles(); + const $microservices = useReadable(microservices) as any; + const history = useHistory(); + const location = useLocation(); + + const _props = props!; + const applicationId = _props.applicationId; + const microserviceId = _props.microserviceId; + const environment = _props.environment; + const podsData = _props.podsData; + + const currentMicroservice = $microservices.find(ms => ms.id === microserviceId); + if (!currentMicroservice) { + const href = `/microservices/application/${applicationId}/${environment}/overview`; + history.push(href); + return null; + } + + // TODO modify when we know how we want to handle state of purchase order data + // Fake it till we are ready + const msName = currentMicroservice.name; + + const [currentTab, setCurrentTab] = useState(1); + const [editMode, setEditMode] = useState(false); + const [currentFiles, setCurrentFiles] = useState([] as string[]); + + + + const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => { + setCurrentTab(newValue); + }; + + useEffect(() => { + (async () => { + try { + const data = await getFiles(applicationId, environment, microserviceId); + setCurrentFiles(data.files); + } catch (e) { + console.error(e); + } + })(); + }, []); + + return ( + + + + + {msName} + + + + + + + + + + + + + + + { + enqueueSnackbar('TODO: Delete microservice', { variant: 'error' }); + }} + className={classes.deleteIcon} + > + + delete + + + + + + + + +

TODO: Config

+ +
+ + + + +
+ + + + + + +
+ ); +}; diff --git a/Source/SelfService/Web/microservice/view.tsx b/Source/SelfService/Web/microservice/view.tsx index 101a8ce25..6af399619 100644 --- a/Source/SelfService/Web/microservice/view.tsx +++ b/Source/SelfService/Web/microservice/view.tsx @@ -11,6 +11,8 @@ import { View as BaseView } from './base/view'; import { View as BusinessMomentsAdaptorView } from './businessMomentsAdaptor/view'; import { View as RawDataLogView } from './rawDataLog/view'; import { View as PurchaseOrderApiView } from './purchaseOrder/view'; +import { View as StaticFilesV1View } from './staticSite/view'; + type Props = { applicationId: string @@ -75,6 +77,10 @@ export const Overview: React.FunctionComponent = (props) => { return ( ); + case 'static-files-v1': + return ( + + ); default: return ( <> @@ -93,16 +99,19 @@ function whichSubView(currentMicroservice: any): string { // Today we try and map subviews based on kind, its not perfect let kind = currentMicroservice.kind; if ( - currentMicroservice && - currentMicroservice.live && - currentMicroservice.live.images && - currentMicroservice.live.images[0] && - currentMicroservice.live.images[0].image && - currentMicroservice.live.images[0].image === '453e04a74f9d42f2b36cd51fa2c83fa3.azurecr.io/dolittle/platform/platform-api:dev-x' + currentMicroservice?.live?.images[0]?.image === '453e04a74f9d42f2b36cd51fa2c83fa3.azurecr.io/dolittle/platform/platform-api:dev-x' ) { kind = 'raw-data-log-ingestor'; } + + if ( + currentMicroservice?.live?.kind === 'static-files-v1' + ) { + kind = 'static-files-v1'; + } + + if (kind === '') { kind = 'simple'; // TODO change } From 03856291dce30de06aac864e265c2a86b5cfaff1 Mon Sep 17 00:00:00 2001 From: freshteapot Date: Fri, 15 Oct 2021 10:28:05 +0200 Subject: [PATCH 2/6] Adding network policy so system-api can reach customer-chris --- Environment/k3d/k8s/networking-policy.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 Environment/k3d/k8s/networking-policy.yml diff --git a/Environment/k3d/k8s/networking-policy.yml b/Environment/k3d/k8s/networking-policy.yml new file mode 100644 index 000000000..d425b2210 --- /dev/null +++ b/Environment/k3d/k8s/networking-policy.yml @@ -0,0 +1,22 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + annotations: + dolittle.io/tenant-id: 453e04a7-4f9d-42f2-b36c-d51fa2c83fa3 + dolittle.io/application-id: 11b6cf47-5d9f-438f-8116-0d9828654657 + labels: + tenant: Customer-Chris + application: Taco + name: all-system-api + namespace: application-11b6cf47-5d9f-438f-8116-0d9828654657 +spec: + podSelector: + matchLabels: + tenant: Customer-Chris + application: Taco + policyTypes: ["Ingress"] + ingress: + - from: + - namespaceSelector: + matchLabels: + system: Api From 16383e29f1c7a0dd883e8b50c5031373d1304c23 Mon Sep 17 00:00:00 2001 From: freshteapot Date: Fri, 15 Oct 2021 13:13:49 +0200 Subject: [PATCH 3/6] Delete + add a little better --- Source/SelfService/Web/api/staticFiles.ts | 19 +- .../Web/microservice/staticSite/files.tsx | 136 ----------- .../staticSite/files/listView.tsx | 121 ++++++++++ .../microservice/staticSite/files/view.tsx | 216 ++++++++++++++++++ .../Web/microservice/staticSite/view.tsx | 24 +- 5 files changed, 353 insertions(+), 163 deletions(-) delete mode 100644 Source/SelfService/Web/microservice/staticSite/files.tsx create mode 100644 Source/SelfService/Web/microservice/staticSite/files/listView.tsx create mode 100644 Source/SelfService/Web/microservice/staticSite/files/view.tsx diff --git a/Source/SelfService/Web/api/staticFiles.ts b/Source/SelfService/Web/api/staticFiles.ts index e54da6da5..2643a1181 100644 --- a/Source/SelfService/Web/api/staticFiles.ts +++ b/Source/SelfService/Web/api/staticFiles.ts @@ -20,7 +20,7 @@ export async function getFiles(applicationId: string, environment: string, micro return body; } -export async function addFile(applicationId: string, environment: string, microserviceId: string, fileName: string, file: File): Promise { +export async function addFile(applicationId: string, environment: string, microserviceId: string, fileName: string, file: File): Promise { const url = `${getServerUrlPrefix()}/application/${applicationId}/environment/${environment.toLowerCase()}/staticFiles/${microserviceId}/add/${fileName}`; const response = await fetch(url, { method: 'POST', @@ -28,12 +28,15 @@ export async function addFile(applicationId: string, environment: string, micros body: file, }); - const body: any = await response.json() as StaticFiles; - return body; + if (response.status !== 201) { + console.error(response); + throw Error('Failed to add file'); + } + return true; } // deleteFile based on the filename that can be found via getFiles -export async function deleteFile(applicationId: string, environment: string, microserviceId: string, fileName: string): Promise { +export async function deleteFile(applicationId: string, environment: string, microserviceId: string, fileName: string): Promise { const url = `${getServerUrlPrefix()}/application/${applicationId}/environment/${environment.toLowerCase()}/staticFiles/${microserviceId}/remove/${fileName}`; // TODO add file const response = await fetch(url, { @@ -41,6 +44,10 @@ export async function deleteFile(applicationId: string, environment: string, mic mode: 'cors', }); - const body: any = await response.json() as StaticFiles; - return body; + + if (response.status !== 200) { + console.error(response); + throw Error('Failed to delete'); + } + return true; } diff --git a/Source/SelfService/Web/microservice/staticSite/files.tsx b/Source/SelfService/Web/microservice/staticSite/files.tsx deleted file mode 100644 index 68f57178f..000000000 --- a/Source/SelfService/Web/microservice/staticSite/files.tsx +++ /dev/null @@ -1,136 +0,0 @@ -// Copyright (c) Dolittle. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -import React from 'react'; -import { makeStyles, createStyles, Theme } from '@material-ui/core/styles'; -import Button from '@material-ui/core/Button'; -import IconButton from '@material-ui/core/IconButton'; -import PhotoCamera from '@material-ui/icons/PhotoCamera'; -import { addFile } from '../../api/staticFiles'; - -import { Button as DolittleButton } from '../../theme/button'; -import { TextField } from '@material-ui/core'; - -const useStyles = makeStyles((theme: Theme) => - createStyles({ - root: { - '& > *': { - margin: theme.spacing(1), - }, - }, - input: { - display: 'none', - }, - }), -); - -// https://stackoverflow.com/questions/40589302/how-to-enable-file-upload-on-reacts-material-ui-simple-input - -interface FormProps { - onClick: React.MouseEventHandler; //(fileName:Blob) => Promise, // callback taking a string and then dispatching a store actions - onChange: React.ChangeEventHandler; - onNameChange: React.ChangeEventHandler; -} - -const UploadButton: React.FunctionComponent = (props) => { - const classes = useStyles(); - - const [fileName, setFileName] = React.useState(''); - return ( -
- - ) => { - setFileName(event.target.value!); - props!.onNameChange(event); - }} - /> - - - - - - - - Upload - -
- ); -}; - -type Props = { - applicationId: string - environment: string - microserviceId: string - files: string[] -}; - -export const Files: React.FunctionComponent = (props) => { - const _props = props!; - const applicationId = _props.applicationId; - const microserviceId = _props.microserviceId; - const environment = _props.environment; - - const [selectedFile, setSelectedFile] = React.useState(null); - const [fileName, setFileName] = React.useState(''); - - const handleCapture = ({ target }: any) => { - setSelectedFile(target.files[0]); - }; - - const handleFileNameChange = ({ target }: any) => { - setFileName(target.value!); - }; - - const handleSubmit = () => { - addFile(applicationId, environment, microserviceId, fileName, selectedFile! as File); - }; - - const listView = _props.files.map((fileName: string) => { - return (

{fileName}

); - }); - - return ( - <> -

TODO: Poormans fileview

-

TODO: Upload file

- - -

TODO: list files

- {listView} -

TODO: Remove file

- - - - - ); -}; diff --git a/Source/SelfService/Web/microservice/staticSite/files/listView.tsx b/Source/SelfService/Web/microservice/staticSite/files/listView.tsx new file mode 100644 index 000000000..69455743d --- /dev/null +++ b/Source/SelfService/Web/microservice/staticSite/files/listView.tsx @@ -0,0 +1,121 @@ +// Copyright (c) Dolittle. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +import React, { useState, useEffect } from 'react'; + +import Table from '@material-ui/core/Table'; +import TableBody from '@material-ui/core/TableBody'; +import TableCell from '@material-ui/core/TableCell'; +import TableContainer from '@material-ui/core/TableContainer'; +import TableHead from '@material-ui/core/TableHead'; +import TableRow from '@material-ui/core/TableRow'; +import Paper from '@material-ui/core/Paper'; +import Box from '@material-ui/core/Box'; + +import { useSnackbar } from 'notistack'; +import OpenInNewIcon from '@material-ui/icons/OpenInNew'; +import DeleteForeverIcon from '@material-ui/icons/DeleteForever'; +import FileCopyIcon from '@material-ui/icons/FileCopy'; + +import { StaticFiles, deleteFile } from '../../../api/staticFiles'; +import { ButtonText } from '../../../theme/buttonText'; +import { CdnInfo } from './view'; + +type ListItem = { + fileName: string + fullUrl: string +}; + +type Props = { + applicationId: string + environment: string + microserviceId: string + cdnInfo: CdnInfo + data: StaticFiles + afterDelete: () => void +}; + + +export const ListView: React.FunctionComponent = (props) => { + const { enqueueSnackbar } = useSnackbar(); + const _props = props!; + + const applicationId = _props.applicationId; + const microserviceId = _props.microserviceId; + const environment = _props.environment; + const data = _props.data; + const cdnInfo = _props.cdnInfo; + + const items: ListItem[] = data.files.map(e => { + // No need for "/" between them + const url = `${cdnInfo.domain}${e}`; + return { + fileName: e, + fullUrl: url, + }; + }); + + const onClickDelete = async (item: ListItem) => { + const fileName = item.fileName.replace(cdnInfo.prefix, ''); + // TODO handle delete + await deleteFile(applicationId, environment, microserviceId, item.fileName); + enqueueSnackbar('item deleted'); + _props.afterDelete(); + }; + + const onClickView = (item: ListItem) => { + enqueueSnackbar('TODO: url to open in new window'); + window.open(item.fullUrl, '_blank'); + }; + + return ( + + + + + + + Name + View + Remove + + + + {items.map((item) => ( + + + { + await navigator.clipboard.writeText(item.fullUrl); + enqueueSnackbar('url copied to clipboard'); + }} /> + onClickView(item)} + > + {item.fileName} + + + + + onClickView(item)} + startIcon={} + > + View + + + + + onClickDelete(item)} + startIcon={} + > + Delete + + + + ))} + +
+
+
+ ); +}; diff --git a/Source/SelfService/Web/microservice/staticSite/files/view.tsx b/Source/SelfService/Web/microservice/staticSite/files/view.tsx new file mode 100644 index 000000000..60ee9421f --- /dev/null +++ b/Source/SelfService/Web/microservice/staticSite/files/view.tsx @@ -0,0 +1,216 @@ +// Copyright (c) Dolittle. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import React, { useState, useEffect } from 'react'; +import { makeStyles, createStyles, Theme } from '@material-ui/core/styles'; +import Button from '@material-ui/core/Button'; +import IconButton from '@material-ui/core/IconButton'; +import PhotoCamera from '@material-ui/icons/PhotoCamera'; +import { Box, TextField } from '@material-ui/core'; + +import { getFiles, addFile, StaticFiles } from '../../../api/staticFiles'; +import { Button as DolittleButton } from '../../../theme/button'; +import { ListView } from './listView'; + + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + '& > *': { + margin: theme.spacing(1), + }, + }, + input: { + display: 'none', + }, + }), +); + +// https://stackoverflow.com/questions/40589302/how-to-enable-file-upload-on-reacts-material-ui-simple-input + +interface FormProps { + onClick: React.MouseEventHandler; //(fileName:Blob) => Promise, // callback taking a string and then dispatching a store actions + onChange: React.ChangeEventHandler; + onNameChange: (string) => void; + cdnInfo: CdnInfo; +} + +const UploadButton: React.FunctionComponent = (props) => { + const classes = useStyles(); + + const [fileName, setFileName] = useState(''); + return ( +
+ + ) => { + setFileName(event.target.value!); + props!.onNameChange(event.target.value!); + }} + /> + + { + const target = event.target as any; + // TODO possible bugs with missing end / + const url = `${props!.cdnInfo.path}${target.files[0].name}`; + setFileName(url); + props!.onChange(event); + props!.onNameChange(url); + }} + /> + + + + + + Upload + +
+ ); +}; + +type Props = { + applicationId: string + environment: string + microserviceId: string +}; + +export type CdnInfo = { + domain: string + prefix: string + path: string +}; + +export const View: React.FunctionComponent = (props) => { + // TODO this should not be hardcoded + // TODO Make sure we remove trailing slash + const cdnInfo = { + domain: 'https://freshteapot-taco.dolittle.cloud', + prefix: '/doc/', + path: '', + } as CdnInfo; + cdnInfo.path = `${cdnInfo.domain}${cdnInfo.prefix}`; + + const _props = props!; + const applicationId = _props.applicationId; + const microserviceId = _props.microserviceId; + const environment = _props.environment; + + const [selectedFile, setSelectedFile] = React.useState(null); + const [fileName, setFileName] = React.useState(''); + const [loading, setLoading] = React.useState(true); + const [runtimeError, setRuntimeError] = React.useState(null as any); + const [currentFiles, setCurrentFiles] = useState({ files: [] } as StaticFiles); + + + const loadData = async () => { + try { + const data = await getFiles(applicationId, environment, microserviceId); + setCurrentFiles(data); + setLoading(false); + } catch (e) { + console.error(e); + setLoading(false); + setRuntimeError(e); + } + }; + + useEffect(() => { + (async () => { + loadData(); + })(); + }, []); + + if (runtimeError) { + return

Error

; + } + + const handleAfterFileDelete = async () => { + console.log('here'); + setLoading(true); + await loadData(); + }; + + const handleCapture = ({ target }: any) => { + setSelectedFile(target.files[0]); + }; + + const handleFileNameChange = (newValue: string) => { + setFileName(newValue); + }; + + const handleSubmit = async () => { + let suffix = fileName.replace(cdnInfo.path, ''); + suffix = suffix.startsWith('/') ? suffix.substring(1) : suffix; + + await addFile(applicationId, environment, microserviceId, suffix, selectedFile! as File); + setLoading(true); + await loadData(); + }; + + + let message: React.ReactNode = null; + + if (loading) { + message =

Loading files

; + } + + if (runtimeError) { + message =

Error loading files

; + } + + return ( + <> + {message} + +

{cdnInfo.path}

+
+ +

Upload file

+ + + {!loading && ( + <> +

List files

+ + + )} + + + ); +}; diff --git a/Source/SelfService/Web/microservice/staticSite/view.tsx b/Source/SelfService/Web/microservice/staticSite/view.tsx index 3ee30073d..ba7ecb4d9 100644 --- a/Source/SelfService/Web/microservice/staticSite/view.tsx +++ b/Source/SelfService/Web/microservice/staticSite/view.tsx @@ -1,6 +1,6 @@ // Copyright (c) Dolittle. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -import React, { useState, useEffect } from 'react'; +import React, { useState } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; import { Grid, @@ -23,8 +23,7 @@ import { useReadable } from 'use-svelte-store'; import { Tab, Tabs } from '../../theme/tabs'; import { DownloadButtons } from '../components/downloadButtons'; -import { getFiles } from '../../api/staticFiles'; -import { Files } from './files'; +import { View as FilesView } from './files/view'; type Props = { @@ -100,26 +99,10 @@ export const View: React.FunctionComponent = (props) => { const msName = currentMicroservice.name; const [currentTab, setCurrentTab] = useState(1); - const [editMode, setEditMode] = useState(false); - const [currentFiles, setCurrentFiles] = useState([] as string[]); - - - const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => { setCurrentTab(newValue); }; - useEffect(() => { - (async () => { - try { - const data = await getFiles(applicationId, environment, microserviceId); - setCurrentFiles(data.files); - } catch (e) { - console.error(e); - } - })(); - }, []); - return ( = (props) => { - From af5457b11089c51b153ff9fca3b3d5a0a313ca03 Mon Sep 17 00:00:00 2001 From: freshteapot Date: Fri, 15 Oct 2021 14:28:51 +0200 Subject: [PATCH 4/6] moving upload and removing the old --- .../microservice/staticSite/files/upload.tsx | 107 ++++++++++++++++++ .../microservice/staticSite/files/view.tsx | 101 ++--------------- 2 files changed, 115 insertions(+), 93 deletions(-) create mode 100644 Source/SelfService/Web/microservice/staticSite/files/upload.tsx diff --git a/Source/SelfService/Web/microservice/staticSite/files/upload.tsx b/Source/SelfService/Web/microservice/staticSite/files/upload.tsx new file mode 100644 index 000000000..b66fbba54 --- /dev/null +++ b/Source/SelfService/Web/microservice/staticSite/files/upload.tsx @@ -0,0 +1,107 @@ +// Copyright (c) Dolittle. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import React, { useState, useEffect } from 'react'; +import { makeStyles, createStyles, Theme } from '@material-ui/core/styles'; +import Button from '@material-ui/core/Button'; +import IconButton from '@material-ui/core/IconButton'; +import PhotoCamera from '@material-ui/icons/PhotoCamera'; +import { TextField } from '@material-ui/core'; +import { Button as DolittleButton } from '../../../theme/button'; +import { CdnInfo } from './view'; + + + + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + '& > *': { + margin: theme.spacing(1), + }, + }, + input: { + display: 'none', + }, + }), +); + +// https://stackoverflow.com/questions/40589302/how-to-enable-file-upload-on-reacts-material-ui-simple-input + +export interface FormProps { + onClick: React.MouseEventHandler; //(fileName:Blob) => Promise, // callback taking a string and then dispatching a store actions + onChange: React.ChangeEventHandler; + onNameChange: (string) => void; + cdnInfo: CdnInfo; + reset: boolean; +} + +export const UploadButton: React.FunctionComponent = (props) => { + const classes = useStyles(); + + const [fileName, setFileName] = useState(''); + + useEffect(() => { + if (props!.reset) { + setFileName(''); + } + + }, [props!.reset]); + + return ( +
+ + ) => { + setFileName(event.target.value!); + props!.onNameChange(event.target.value!); + }} + /> + + { + const target = event.target as any; + // TODO possible bugs with missing end / + const url = `${props!.cdnInfo.path}${target.files[0].name}`; + setFileName(url); + props!.onChange(event); + props!.onNameChange(url); + }} + /> + + + + + + Upload + +
+ ); +}; diff --git a/Source/SelfService/Web/microservice/staticSite/files/view.tsx b/Source/SelfService/Web/microservice/staticSite/files/view.tsx index 60ee9421f..c39eb5c83 100644 --- a/Source/SelfService/Web/microservice/staticSite/files/view.tsx +++ b/Source/SelfService/Web/microservice/staticSite/files/view.tsx @@ -2,100 +2,10 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. import React, { useState, useEffect } from 'react'; -import { makeStyles, createStyles, Theme } from '@material-ui/core/styles'; -import Button from '@material-ui/core/Button'; -import IconButton from '@material-ui/core/IconButton'; -import PhotoCamera from '@material-ui/icons/PhotoCamera'; -import { Box, TextField } from '@material-ui/core'; - +import { Box } from '@material-ui/core'; import { getFiles, addFile, StaticFiles } from '../../../api/staticFiles'; -import { Button as DolittleButton } from '../../../theme/button'; import { ListView } from './listView'; - - -const useStyles = makeStyles((theme: Theme) => - createStyles({ - root: { - '& > *': { - margin: theme.spacing(1), - }, - }, - input: { - display: 'none', - }, - }), -); - -// https://stackoverflow.com/questions/40589302/how-to-enable-file-upload-on-reacts-material-ui-simple-input - -interface FormProps { - onClick: React.MouseEventHandler; //(fileName:Blob) => Promise, // callback taking a string and then dispatching a store actions - onChange: React.ChangeEventHandler; - onNameChange: (string) => void; - cdnInfo: CdnInfo; -} - -const UploadButton: React.FunctionComponent = (props) => { - const classes = useStyles(); - - const [fileName, setFileName] = useState(''); - return ( -
- - ) => { - setFileName(event.target.value!); - props!.onNameChange(event.target.value!); - }} - /> - - { - const target = event.target as any; - // TODO possible bugs with missing end / - const url = `${props!.cdnInfo.path}${target.files[0].name}`; - setFileName(url); - props!.onChange(event); - props!.onNameChange(url); - }} - /> - - - - - - Upload - -
- ); -}; +import { UploadButton } from './upload'; type Props = { applicationId: string @@ -127,6 +37,8 @@ export const View: React.FunctionComponent = (props) => { const [selectedFile, setSelectedFile] = React.useState(null); const [fileName, setFileName] = React.useState(''); const [loading, setLoading] = React.useState(true); + const [reset, setReset] = React.useState(false); + const [runtimeError, setRuntimeError] = React.useState(null as any); const [currentFiles, setCurrentFiles] = useState({ files: [] } as StaticFiles); @@ -174,6 +86,9 @@ export const View: React.FunctionComponent = (props) => { await addFile(applicationId, environment, microserviceId, suffix, selectedFile! as File); setLoading(true); await loadData(); + setFileName(''); + setSelectedFile(null); + setReset(true); }; @@ -195,7 +110,7 @@ export const View: React.FunctionComponent = (props) => {

Upload file

- + {!loading && ( <> From 86998e1c6a5793dd696f54917846eb15062f1db0 Mon Sep 17 00:00:00 2001 From: freshteapot Date: Fri, 22 Oct 2021 08:30:57 +0200 Subject: [PATCH 5/6] fixup --- Source/SelfService/Web/api/staticFiles.ts | 18 +++++++++++------- .../microservice/staticSite/files/listView.tsx | 4 +--- .../Web/microservice/staticSite/files/view.tsx | 8 ++++---- .../Web/microservice/staticSite/view.tsx | 2 -- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Source/SelfService/Web/api/staticFiles.ts b/Source/SelfService/Web/api/staticFiles.ts index 2643a1181..4e437e3e4 100644 --- a/Source/SelfService/Web/api/staticFiles.ts +++ b/Source/SelfService/Web/api/staticFiles.ts @@ -7,7 +7,7 @@ export type StaticFiles = { files: string[] }; - +// @throws {Error} export async function getFiles(applicationId: string, environment: string, microserviceId: string): Promise { const url = `${getServerUrlPrefix()}/application/${applicationId}/environment/${environment.toLowerCase()}/staticFiles/${microserviceId}/list`; @@ -16,11 +16,16 @@ export async function getFiles(applicationId: string, environment: string, micro mode: 'cors', }); - const body: any = await response.json() as StaticFiles; - return body; + if (response.status !== 200) { + console.error(response); + throw Error('Failed to get files'); + } + + return await response.json() as StaticFiles; } -export async function addFile(applicationId: string, environment: string, microserviceId: string, fileName: string, file: File): Promise { +// @throws {Error} +export async function addFile(applicationId: string, environment: string, microserviceId: string, fileName: string, file: File): Promise { const url = `${getServerUrlPrefix()}/application/${applicationId}/environment/${environment.toLowerCase()}/staticFiles/${microserviceId}/add/${fileName}`; const response = await fetch(url, { method: 'POST', @@ -32,11 +37,11 @@ export async function addFile(applicationId: string, environment: string, micros console.error(response); throw Error('Failed to add file'); } - return true; } // deleteFile based on the filename that can be found via getFiles -export async function deleteFile(applicationId: string, environment: string, microserviceId: string, fileName: string): Promise { +// @throws {Error} +export async function deleteFile(applicationId: string, environment: string, microserviceId: string, fileName: string): Promise { const url = `${getServerUrlPrefix()}/application/${applicationId}/environment/${environment.toLowerCase()}/staticFiles/${microserviceId}/remove/${fileName}`; // TODO add file const response = await fetch(url, { @@ -49,5 +54,4 @@ export async function deleteFile(applicationId: string, environment: string, mic console.error(response); throw Error('Failed to delete'); } - return true; } diff --git a/Source/SelfService/Web/microservice/staticSite/files/listView.tsx b/Source/SelfService/Web/microservice/staticSite/files/listView.tsx index 69455743d..59751febc 100644 --- a/Source/SelfService/Web/microservice/staticSite/files/listView.tsx +++ b/Source/SelfService/Web/microservice/staticSite/files/listView.tsx @@ -45,7 +45,7 @@ export const ListView: React.FunctionComponent = (props) => { const data = _props.data; const cdnInfo = _props.cdnInfo; - const items: ListItem[] = data.files.map(e => { + const items = data.files.map(e => { // No need for "/" between them const url = `${cdnInfo.domain}${e}`; return { @@ -55,8 +55,6 @@ export const ListView: React.FunctionComponent = (props) => { }); const onClickDelete = async (item: ListItem) => { - const fileName = item.fileName.replace(cdnInfo.prefix, ''); - // TODO handle delete await deleteFile(applicationId, environment, microserviceId, item.fileName); enqueueSnackbar('item deleted'); _props.afterDelete(); diff --git a/Source/SelfService/Web/microservice/staticSite/files/view.tsx b/Source/SelfService/Web/microservice/staticSite/files/view.tsx index c39eb5c83..4f472a368 100644 --- a/Source/SelfService/Web/microservice/staticSite/files/view.tsx +++ b/Source/SelfService/Web/microservice/staticSite/files/view.tsx @@ -34,10 +34,10 @@ export const View: React.FunctionComponent = (props) => { const microserviceId = _props.microserviceId; const environment = _props.environment; - const [selectedFile, setSelectedFile] = React.useState(null); - const [fileName, setFileName] = React.useState(''); - const [loading, setLoading] = React.useState(true); - const [reset, setReset] = React.useState(false); + const [selectedFile, setSelectedFile] = useState(null); + const [fileName, setFileName] = useState(''); + const [loading, setLoading] = useState(true); + const [reset, setReset] = useState(false); const [runtimeError, setRuntimeError] = React.useState(null as any); const [currentFiles, setCurrentFiles] = useState({ files: [] } as StaticFiles); diff --git a/Source/SelfService/Web/microservice/staticSite/view.tsx b/Source/SelfService/Web/microservice/staticSite/view.tsx index ba7ecb4d9..654159b70 100644 --- a/Source/SelfService/Web/microservice/staticSite/view.tsx +++ b/Source/SelfService/Web/microservice/staticSite/view.tsx @@ -94,8 +94,6 @@ export const View: React.FunctionComponent = (props) => { return null; } - // TODO modify when we know how we want to handle state of purchase order data - // Fake it till we are ready const msName = currentMicroservice.name; const [currentTab, setCurrentTab] = useState(1); From 04edc814fcbc52bafe53da2b345cc44a1fc91d87 Mon Sep 17 00:00:00 2001 From: freshteapot Date: Fri, 22 Oct 2021 08:31:03 +0200 Subject: [PATCH 6/6] Fixup - Upload hanlder shows error if file not selected - TODO about removing logic pinned to an out of date image --- .../microservice/staticSite/files/upload.tsx | 36 ++++++------------- .../microservice/staticSite/files/view.tsx | 9 +++++ Source/SelfService/Web/microservice/view.tsx | 6 ++-- 3 files changed, 23 insertions(+), 28 deletions(-) diff --git a/Source/SelfService/Web/microservice/staticSite/files/upload.tsx b/Source/SelfService/Web/microservice/staticSite/files/upload.tsx index b66fbba54..f5b27d4fc 100644 --- a/Source/SelfService/Web/microservice/staticSite/files/upload.tsx +++ b/Source/SelfService/Web/microservice/staticSite/files/upload.tsx @@ -1,18 +1,13 @@ // Copyright (c) Dolittle. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { makeStyles, createStyles, Theme } from '@material-ui/core/styles'; -import Button from '@material-ui/core/Button'; -import IconButton from '@material-ui/core/IconButton'; -import PhotoCamera from '@material-ui/icons/PhotoCamera'; import { TextField } from '@material-ui/core'; import { Button as DolittleButton } from '../../../theme/button'; import { CdnInfo } from './view'; - - const useStyles = makeStyles((theme: Theme) => createStyles({ root: { @@ -48,9 +43,11 @@ export const UploadButton: React.FunctionComponent = (props) => { }, [props!.reset]); + + const inputFileRef = useRef(null); + return (
- = (props) => { props!.onNameChange(event.target.value!); }} /> - = (props) => { props!.onNameChange(url); }} /> - - - + inputFileRef?.current?.click()} + > + Select File + Upload -
+ ); }; diff --git a/Source/SelfService/Web/microservice/staticSite/files/view.tsx b/Source/SelfService/Web/microservice/staticSite/files/view.tsx index 4f472a368..63d3ddeaf 100644 --- a/Source/SelfService/Web/microservice/staticSite/files/view.tsx +++ b/Source/SelfService/Web/microservice/staticSite/files/view.tsx @@ -3,6 +3,8 @@ import React, { useState, useEffect } from 'react'; import { Box } from '@material-ui/core'; +import { useSnackbar } from 'notistack'; + import { getFiles, addFile, StaticFiles } from '../../../api/staticFiles'; import { ListView } from './listView'; import { UploadButton } from './upload'; @@ -20,6 +22,8 @@ export type CdnInfo = { }; export const View: React.FunctionComponent = (props) => { + const { enqueueSnackbar } = useSnackbar(); + // TODO this should not be hardcoded // TODO Make sure we remove trailing slash const cdnInfo = { @@ -80,6 +84,11 @@ export const View: React.FunctionComponent = (props) => { }; const handleSubmit = async () => { + if (!selectedFile) { + enqueueSnackbar('You need to select a file first', { variant: 'error' }); + return; + } + let suffix = fileName.replace(cdnInfo.path, ''); suffix = suffix.startsWith('/') ? suffix.substring(1) : suffix; diff --git a/Source/SelfService/Web/microservice/view.tsx b/Source/SelfService/Web/microservice/view.tsx index 6af399619..4ab727b3a 100644 --- a/Source/SelfService/Web/microservice/view.tsx +++ b/Source/SelfService/Web/microservice/view.tsx @@ -98,6 +98,8 @@ export const Overview: React.FunctionComponent = (props) => { function whichSubView(currentMicroservice: any): string { // Today we try and map subviews based on kind, its not perfect let kind = currentMicroservice.kind; + // TODO this code block needs removing + // Kind via live should be possible since we add annotation "dolittle.io/microservice-kind" if ( currentMicroservice?.live?.images[0]?.image === '453e04a74f9d42f2b36cd51fa2c83fa3.azurecr.io/dolittle/platform/platform-api:dev-x' ) { @@ -105,9 +107,7 @@ function whichSubView(currentMicroservice: any): string { } - if ( - currentMicroservice?.live?.kind === 'static-files-v1' - ) { + if (currentMicroservice?.live?.kind === 'static-files-v1') { kind = 'static-files-v1'; }