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 diff --git a/Source/SelfService/Web/api/staticFiles.ts b/Source/SelfService/Web/api/staticFiles.ts new file mode 100644 index 000000000..4e437e3e4 --- /dev/null +++ b/Source/SelfService/Web/api/staticFiles.ts @@ -0,0 +1,57 @@ +// 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[] +}; + +// @throws {Error} +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', + }); + + if (response.status !== 200) { + console.error(response); + throw Error('Failed to get files'); + } + + return await response.json() as StaticFiles; +} + +// @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', + mode: 'cors', + body: file, + }); + + if (response.status !== 201) { + console.error(response); + throw Error('Failed to add file'); + } +} + +// deleteFile based on the filename that can be found via getFiles +// @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, { + method: 'DELETE', + mode: 'cors', + }); + + + if (response.status !== 200) { + console.error(response); + throw Error('Failed to delete'); + } +} 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..59751febc --- /dev/null +++ b/Source/SelfService/Web/microservice/staticSite/files/listView.tsx @@ -0,0 +1,119 @@ +// 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 = data.files.map(e => { + // No need for "/" between them + const url = `${cdnInfo.domain}${e}`; + return { + fileName: e, + fullUrl: url, + }; + }); + + const onClickDelete = async (item: ListItem) => { + 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/upload.tsx b/Source/SelfService/Web/microservice/staticSite/files/upload.tsx new file mode 100644 index 000000000..f5b27d4fc --- /dev/null +++ b/Source/SelfService/Web/microservice/staticSite/files/upload.tsx @@ -0,0 +1,93 @@ +// 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, useRef } from 'react'; +import { makeStyles, createStyles, Theme } from '@material-ui/core/styles'; +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]); + + + const inputFileRef = useRef(null); + + 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); + }} + /> + + 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 new file mode 100644 index 000000000..63d3ddeaf --- /dev/null +++ b/Source/SelfService/Web/microservice/staticSite/files/view.tsx @@ -0,0 +1,140 @@ +// 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 { Box } from '@material-ui/core'; +import { useSnackbar } from 'notistack'; + +import { getFiles, addFile, StaticFiles } from '../../../api/staticFiles'; +import { ListView } from './listView'; +import { UploadButton } from './upload'; + +type Props = { + applicationId: string + environment: string + microserviceId: string +}; + +export type CdnInfo = { + domain: string + prefix: string + path: string +}; + +export const View: React.FunctionComponent = (props) => { + const { enqueueSnackbar } = useSnackbar(); + + // 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] = 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); + + + 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 () => { + 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; + + await addFile(applicationId, environment, microserviceId, suffix, selectedFile! as File); + setLoading(true); + await loadData(); + setFileName(''); + setSelectedFile(null); + setReset(true); + }; + + + 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 new file mode 100644 index 000000000..654159b70 --- /dev/null +++ b/Source/SelfService/Web/microservice/staticSite/view.tsx @@ -0,0 +1,186 @@ +// 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 } 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 { View as FilesView } from './files/view'; + + +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; + } + + const msName = currentMicroservice.name; + + const [currentTab, setCurrentTab] = useState(1); + const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => { + setCurrentTab(newValue); + }; + + 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..4ab727b3a 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 ( <> @@ -92,17 +98,20 @@ 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 && - 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 }