Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

StaticFiles microservice with a files tab #125

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions Environment/k3d/k8s/networking-policy.yml
Original file line number Diff line number Diff line change
@@ -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
57 changes: 57 additions & 0 deletions Source/SelfService/Web/api/staticFiles.ts
Original file line number Diff line number Diff line change
@@ -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<StaticFiles> {
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<void> {
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<void> {
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');
}
}
119 changes: 119 additions & 0 deletions Source/SelfService/Web/microservice/staticSite/files/listView.tsx
Original file line number Diff line number Diff line change
@@ -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> = (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<ListItem>(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 (

<Box component={Paper} m={2}>
<TableContainer>
<Table size="small" aria-label="simple table">
<TableHead>
<TableRow>
<TableCell align="left">Name</TableCell>
<TableCell align="right">View</TableCell>
<TableCell align="right">Remove</TableCell>
</TableRow>
</TableHead>
<TableBody>
{items.map((item) => (
<TableRow key={item.fileName}>
<TableCell align="left">
<FileCopyIcon onClick={async () => {
await navigator.clipboard.writeText(item.fullUrl);
enqueueSnackbar('url copied to clipboard');
}} />
<a
onClick={() => onClickView(item)}
>
{item.fileName}
</a>
</TableCell>

<TableCell align="right" title={item.fullUrl}>
<ButtonText
onClick={() => onClickView(item)}
startIcon={<OpenInNewIcon />}
>
View
</ButtonText>
</TableCell>

<TableCell align="right">
<ButtonText
onClick={() => onClickDelete(item)}
startIcon={<DeleteForeverIcon />}
>
Delete
</ButtonText>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Box >
);
};
93 changes: 93 additions & 0 deletions Source/SelfService/Web/microservice/staticSite/files/upload.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLSpanElement>; //(fileName:Blob) => Promise<void>, // callback taking a string and then dispatching a store actions
onChange: React.ChangeEventHandler<HTMLInputElement>;
onNameChange: (string) => void;
cdnInfo: CdnInfo;
reset: boolean;
}

export const UploadButton: React.FunctionComponent<FormProps> = (props) => {
const classes = useStyles();

const [fileName, setFileName] = useState('');

useEffect(() => {
if (props!.reset) {
setFileName('');
}

}, [props!.reset]);


const inputFileRef = useRef<HTMLInputElement>(null);

return (
<div className={classes.root}>
<TextField
required
id='outlined-required'
label='file name'
variant='outlined'

value={fileName}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setFileName(event.target.value!);
props!.onNameChange(event.target.value!);
}}
/>
<input
ref={inputFileRef}
accept="image/*"
className={classes.input}
id="contained-button-file"
multiple
type="file"
onChange={(event) => {
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);
}}
/>

<DolittleButton
onClick={() => inputFileRef?.current?.click()}
>
Select File
</DolittleButton>

<DolittleButton
onClick={props!.onClick}
>
Upload
</DolittleButton>
</div >
);
};
Loading