Compare commits

...

17 Commits

Author SHA1 Message Date
Sipachev Igor
38a3d4b564 Сгенерировал новую версию плагина 2023-04-24 11:17:11 +07:00
Sipachev Igor
906043d52d Добавил несколько фильтров 2023-04-24 11:16:58 +07:00
26b0881a46 Собрал новую сборку 2023-04-15 00:18:35 +07:00
931042e716 Поправил типы 2023-04-15 00:18:13 +07:00
5f59bbe18f Добавил доп информацию 2023-04-15 00:11:35 +07:00
4cb539d58a Починил настроил лоадер 2023-04-14 23:52:53 +07:00
c852ef14dd Починил ошибки в консоли 2023-04-14 23:31:13 +07:00
3e8f3943dc Добавил иконку отображающую заблокирован ли процесс
Добавил возможность разблокировать
2023-04-09 11:36:45 +07:00
4dadbec6e8 Доработал смену токена 2023-04-09 00:29:12 +07:00
fbf3c06efd Поправил логику вывода прогресса
Добавил апрув
2023-04-09 00:28:41 +07:00
473a9a1993 Файл стилей для компонент 2023-04-08 23:29:33 +07:00
30c711add4 Вынес токены в компонент 2023-04-08 23:26:52 +07:00
b7fef53ad7 Оптимизировал добавление плюс настроил реактивность для списка 2023-04-08 23:25:17 +07:00
3c76c7dc09 Оптимизировал проверки 2023-04-08 23:24:51 +07:00
4f02f56f85 удалил лишний перенос 2023-04-08 17:12:10 +07:00
0bc9aad4cf Поправил формирвоание адреса 2023-04-08 17:11:45 +07:00
92fd933c60 перевод на jwt 2023-04-06 16:29:08 +07:00
29 changed files with 725 additions and 233 deletions

17
api/authentication.ts Normal file
View File

@ -0,0 +1,17 @@
export interface AuthenticationInterface {
key: string
token(): string
}
export class BearerAuthentication implements AuthenticationInterface {
key = 'Authorization'
_token: string
constructor(token: string) {
this._token = token
}
token(): string {
return `Bearer ${this._token}`;
}
}

View File

@ -1,3 +1,5 @@
import {AuthenticationInterface} from "./authentication";
export enum Method {
GET = 'GET',
POST = 'POST',
@ -9,10 +11,6 @@ export let hasQuery = (method: Method) => {
return Method.GET === method
}
export interface ClientOptions {
baseUrl: string
}
export interface SchemaInterface {
url: string,
method: Method
@ -25,12 +23,23 @@ export class Context {
memoryKey?: string
}
interface SchemaClientOptionsInterface {
baseUrl?: string
authentication?: AuthenticationInterface
}
export class SchemaClient {
baseUrl: string | null = null
authentication: AuthenticationInterface | null = null
context: Context | null = null
memory: Record<string, any> = {}
constructor(options: SchemaClientOptionsInterface) {
this.baseUrl = options.baseUrl || null
this.authentication = options.authentication || null
}
debouncing(time: number)
{
let context = this.grabContext()
@ -47,6 +56,10 @@ export class SchemaClient {
}
async send(schema: SchemaInterface, data: any) {
if (this.baseUrl === null) {
throw new Error('Base url not defined!');
}
let context = this.context
this.context = null
@ -64,12 +77,22 @@ export class SchemaClient {
preparedUrl += '?' + new URLSearchParams(preparedData)
}
let token = ''
if (typeof localStorage !== 'undefined') {
let tokenId = localStorage.getItem('token')
token = localStorage.getItem(tokenId || '') || ''
}
let headers: Record<string, any> = {
...(schema.contentType ? {'Content-Type': schema.contentType} : {}),
'X-Plugin-Token': 'passw0rd',
}
if (this.authentication) {
headers[this.authentication.key] = this.authentication.token();
}
let response = await fetch(preparedUrl, {
method: schema.method.toString(),
headers: {
...(schema.contentType ? {'Content-Type': schema.contentType} : {}),
'X-Plugin-Token': 'passw0rd'
},
headers,
body: hasQuery(schema.method) ? null : JSON.stringify(preparedData)
});

View File

@ -0,0 +1,3 @@
export interface ApproveProcessRequest {
id: string
}

View File

@ -0,0 +1,3 @@
export interface UnlockProcessRequest {
id: string
}

View File

@ -37,6 +37,8 @@ export interface ProcessInterface {
canRepeat: boolean
canStop: boolean
canKill: boolean
canApprove: boolean
canUnlock: boolean
}
export enum Status {

View File

@ -0,0 +1,10 @@
import {Method, SchemaInterface} from "../../schema-client";
class ApproveProcessSchema implements SchemaInterface {
method = Method.POST
url = '/system-monitoring/processes/{id}/approve'
contentType = null
}
export let approveProcessSchema = new ApproveProcessSchema()
export default approveProcessSchema

View File

@ -0,0 +1,10 @@
import {Method, SchemaInterface} from "../../schema-client";
class UnlockProcessSchema implements SchemaInterface {
method = Method.POST
url = '/system-monitoring/processes/{id}/unlock'
contentType = null
}
export let unlockProcessSchema = new UnlockProcessSchema()
export default unlockProcessSchema

View File

@ -8,7 +8,9 @@ import {CommandInterface} from "./responses/comamnds";
import commandsSchema from "./schemas/commands";
import runCommandsSchema from "./schemas/run-commands";
import {RepeatProcessRequest} from "./requests/repeat-process";
import {ApproveProcessRequest} from "./requests/approve-process";
import repeatProcessSchema from "./schemas/repeat-process";
import approveProcessSchema from "./schemas/approve-process";
import processOutputSchema from "./schemas/process-output";
import {ProcessOutputRequest} from "./requests/process-ouput";
import playProcessSchema from "./schemas/play-process";
@ -16,12 +18,10 @@ import pauseProcessSchema from "./schemas/pause-process";
import stopProcessSchema from "./schemas/stop-process";
import killProcessSchema from "./schemas/kill-process";
import commandSchema from "./schemas/command";
let baseUrl = typeof location !== 'undefined' && location.origin.includes('.wallester.') ? location.origin : 'http://fmw.sipachev.sv'
import {UnlockProcessRequest} from "./requests/unlock-process";
import unlockProcessSchema from "./schemas/unlock-process";
export class SMClient extends SchemaClient {
baseUrl = baseUrl
async getCommands(): Promise<ResponseInterface<CommandInterface[]>> {
let { responseData, headers } = await this.send(commandsSchema, {})
return {
@ -56,6 +56,22 @@ export class SMClient extends SchemaClient {
}
}
async approveProcess(data: ApproveProcessRequest): Promise<ResponseInterface> {
let { responseData, headers } = await this.send(approveProcessSchema, data)
return {
data: responseData as ProcessInterface[],
headers: headers
}
}
async unlockProcess(data: UnlockProcessRequest): Promise<ResponseInterface> {
let { responseData, headers } = await this.send(unlockProcessSchema, data)
return {
data: responseData as ProcessInterface[],
headers: headers
}
}
async repeatProcess(data: RepeatProcessRequest): Promise<ResponseInterface> {
let { responseData, headers } = await this.send(repeatProcessSchema, data)
return {
@ -104,6 +120,3 @@ export class SMClient extends SchemaClient {
}
}
}
export let smClient = new SMClient
export default smClient

View File

@ -1,14 +1,13 @@
import styles from './styles.module.css'
import {Table, TableBody, TableCell, TableHead, TableRow, IconButton, Autocomplete, TextField} from "@mui/material";
import {IconButton, Autocomplete, TextField} from "@mui/material";
import Command from "./elements/command";
import {useContext, useEffect, useState} from "react";
import PlayCircleOutline from '@mui/icons-material/PlayCircleOutline';
import ConfirmDialog from "../confirm-dialog";
import TabContext from "../../../context/tab";
import {TabEnum} from "../../../pages";
import smClient from "../../../api/sm/sm-client";
import Send from "@mui/icons-material/Send"
import {CommandInterface} from "../../../api/sm/responses/comamnds";
import {useApi} from "../../../hooks/use-api";
export default function Commands() {
const {setTab} = useContext(TabContext)
@ -18,9 +17,10 @@ export default function Commands() {
const [argumentList, setArgumentList] = useState<Record<string, any>>({});
const [value, setValue] = useState<string | null>('');
const [open, setOpen] = useState<boolean>(false);
const api = useApi()
let refreshCommands = async () => {
const { data: commands } = await smClient.useMemory().getCommands()
const { data: commands } = await api.useMemory().getCommands()
setCommands(commands)
}
@ -34,6 +34,7 @@ export default function Commands() {
}, [value])
let variants = commands.map((command: CommandInterface, index: number) => command.name);
variants.push('')
let callback = (name: string, optionList: Record<string, any>, argumentList: Record<string, any>) => {
setOptionList(optionList)
@ -48,7 +49,7 @@ export default function Commands() {
return
}
lock = true
await smClient.runCommand({
await api.runCommand({
commandName: selectedCommand.name,
options: optionList,
arguments: argumentList,

View File

@ -1,5 +1,5 @@
import styles from './styles.module.css'
import {useEffect, useState} from "react"
import {useContext, useEffect, useState} from "react"
import DeleteForeverOutlined from "@mui/icons-material/DeleteForeverOutlined"
import StopCircleOutlined from "@mui/icons-material/StopCircleOutlined"
import PauseCircleOutline from "@mui/icons-material/PauseCircleOutline"
@ -10,6 +10,10 @@ import ErrorOutline from "@mui/icons-material/ErrorOutline"
import ReplayOutlined from "@mui/icons-material/ReplayOutlined"
import RunCircleOutlined from "@mui/icons-material/RunCircleOutlined"
import FactCheckOutlined from "@mui/icons-material/FactCheckOutlined"
import ThumbUpAltOutlined from '@mui/icons-material/ThumbUpAltOutlined'
import LockOutlined from '@mui/icons-material/LockOutlined';
import LockOpenOutlined from '@mui/icons-material/LockOpenOutlined';
import {
Box,
TextField,
@ -25,11 +29,12 @@ import {
TablePagination, Autocomplete
} from "@mui/material"
import ConfirmDialog from "../confirm-dialog";
import smClient from "../../../api/sm/sm-client";
import {ProcessInterface, Status} from "../../../api/sm/responses/processes";
import Command from "../commands/elements/command";
import {CommandInterface} from "../../../api/sm/responses/comamnds";
import Grid from '@mui/material/Grid';
import {useApi} from "../../../hooks/use-api";
import Context from "../../../context/token";
enum Action {
Run,
@ -38,15 +43,18 @@ enum Action {
Kill,
Play,
Pause,
Approve,
Unlock,
}
export default function Processes() {
const {token} = useContext(Context)
const [processes, setProcesses] = useState<ProcessInterface[]>([]);
const [commands, setCommands] = useState<CommandInterface[]>([]);
const [page, setPage] = useState<number>(0);
const [count, setCount] = useState<number>(0);
const [type, setType] = useState<string | null>('');
const [name, setName] = useState<string | null>('');
const [type, setType] = useState<string>('');
const [name, setName] = useState<string>('');
const [loading, setLoading] = useState<boolean>(true);
const [modalLoading, setModalLoading] = useState<boolean>(true);
const [action, setAction] = useState<Action | null>(null);
@ -54,11 +62,13 @@ export default function Processes() {
const [selectedProcess, setSelectedProcess] = useState<ProcessInterface | null>(null);
const [optionList, setOptionList] = useState<Record<string, any>>({});
const [argumentList, setArgumentList] = useState<Record<string, any>>({});
const api = useApi()
let variants = commands.map((command: CommandInterface, index: number) => command.name);
variants.push('')
let refreshCommands = async () => {
const { data: commands } = await smClient.useMemory().getCommands()
const { data: commands } = await api.useMemory().getCommands()
setCommands(commands)
}
@ -80,7 +90,7 @@ export default function Processes() {
if (!!name) {
data['name'] = name
}
const { data: processes, headers } = await smClient.getProcesses({
const { data: processes, headers } = await api.getProcesses({
...data,
page: page + 1,
limit: 20,
@ -91,7 +101,7 @@ export default function Processes() {
refreshLock = false
}
let output = async (process: ProcessInterface) => {
const { data: output } = await smClient.getProcessOutput({
const { data: output } = await api.getProcessOutput({
id: process.id
})
@ -115,6 +125,12 @@ export default function Processes() {
}, [action]);
let isFinished = (process: ProcessInterface) => process.cancelledAt || process.completedAt
let isCancelled = (process: ProcessInterface) => process.cancelledAt
let isRunning = (process: ProcessInterface) => process.startedAt && !isFinished(process)
const processName = (process: ProcessInterface) => {
return process.name + ' ' + JSON.stringify(process.options)+ ' ' + JSON.stringify(process.arguments)
}
const handleChangePage = (event: any, page: number) => {
setPage(page);
@ -136,7 +152,7 @@ export default function Processes() {
lock = true
if (action === Action.Run) {
await smClient.runCommand({
await api.runCommand({
commandName: selectedProcess.name,
options: optionList,
arguments: argumentList,
@ -144,8 +160,20 @@ export default function Processes() {
})
}
if (action === Action.Approve) {
await api.approveProcess({
id: selectedProcess.id,
})
}
if (action === Action.Unlock) {
await api.unlockProcess({
id: selectedProcess.id,
})
}
if (action === Action.Repeat) {
await smClient.repeatProcess({
await api.repeatProcess({
id: selectedProcess.id,
requestId: dialogId
})
@ -176,10 +204,11 @@ export default function Processes() {
<Autocomplete
value={type}
onChange={(event: any, newValue: string | null) => {
setType(newValue);
setType(newValue || '');
setLoading(true)
}}
disablePortal
options={['None', "Running", "History"]}
options={['None', "Running", "History", "Locked", 'Need_Approve', '']}
renderInput={(params) => <TextField {...params} label="Type"/>}
/>
</Grid>
@ -189,6 +218,7 @@ export default function Processes() {
value={name}
onChange={(event: any, newValue: string | null) => {
setName(newValue || '');
setLoading(true)
}}
disablePortal
options={variants}
@ -210,14 +240,16 @@ export default function Processes() {
<TableBody>
{processes.map((process: ProcessInterface, index: number) => (
<TableRow key={process.id}>
<TableCell>{process.name}</TableCell>
<TableCell title={processName(process)}>{process.name}</TableCell>
<TableCell>
{!process.progress && !isFinished(process) && <LinearProgress/>}
{!process.progress && isFinished(process) && <LinearProgress variant="determinate" value={100}/>}
{!process.progress && !isFinished(process) && isRunning(process) && <LinearProgress/>}
{!process.progress && !isFinished(process) && !isRunning(process) && <LinearProgress variant="determinate" value={0}/>}
{!process.progress && isFinished(process) && !isCancelled(process) && <LinearProgress variant="determinate" value={100}/>}
{!process.progress && isFinished(process) && isCancelled(process) && <LinearProgress variant="determinate" value={0}/>}
{process.progress && <LinearProgress variant="determinate" value={process.progress.percent}/>}
{process.progress && <span>
{`${process.progress.progress}`} / {`${process.progress.total}`} - {process.progress.percent}% [{process.progress.memory}] / {process.progress.remaining}
</span>}
{`${process.progress.progress}`} / {`${process.progress.total}`} - {process.progress.percent}% [{process.progress.memory}] / {process.progress.remaining}
</span>}
{process.canPlay && <IconButton onClick={() => openDialog(process, Action.Play)} title={`Play`} aria-label="Play">
<PlayCircleOutline/>
</IconButton>}
@ -244,12 +276,21 @@ export default function Processes() {
{Status.Wait === process.status && <IconButton title={`Wait`} aria-label="Wait">
<HourglassEmptyOutlined/>
</IconButton>}
{process.lock && <IconButton title={`Locked`} aria-label="Locked">
<LockOutlined/>
</IconButton>}
</TableCell>
<TableCell>
{process.canRepeat && <IconButton onClick={() => openDialog(process, Action.Repeat)} title={`Repeat`} aria-label="Repeat">
{process.canUnlock && token && token?.permissions.indexOf('unlock_process') > -1 && <IconButton onClick={() => openDialog(process, Action.Unlock)} title={`Unlock`} aria-label="Unlock">
<LockOpenOutlined/>
</IconButton>}
{process.canApprove && token && token?.permissions.indexOf('approve_process') > -1 && <IconButton onClick={() => openDialog(process, Action.Approve)} title={`Approve`} aria-label="Approve">
<ThumbUpAltOutlined/>
</IconButton>}
{process.canRepeat && token && token?.permissions.indexOf('run_command') > -1 && <IconButton onClick={() => openDialog(process, Action.Repeat)} title={`Repeat`} aria-label="Repeat">
<ReplayOutlined/>
</IconButton>}
{process.canKill && <IconButton onClick={() => openDialog(process, Action.Kill)} title={`Kill`} aria-label="Kill">
{process.canKill && token && token?.permissions.indexOf('kill_process') > -1 && <IconButton onClick={() => openDialog(process, Action.Kill)} title={`Kill`} aria-label="Kill">
<DeleteForeverOutlined/>
</IconButton>}
{process?.outputId && <IconButton title={`Output`} onClick={() => output(process)} aria-label="Output">
@ -257,7 +298,7 @@ export default function Processes() {
</IconButton>}
</TableCell>
<TableCell title={new Date(process.createdAt).toLocaleString()}>
{new Date(process.createdAt).toLocaleDateString()}
{new Date(process.createdAt).toLocaleString()}
</TableCell>
</TableRow>
))}
@ -268,6 +309,7 @@ export default function Processes() {
component="div"
count={count}
rowsPerPage={20}
rowsPerPageOptions={[10, 20, 50, 100]}
page={page}
onPageChange={handleChangePage}
/>
@ -278,7 +320,7 @@ export default function Processes() {
modifyCallback={async () => {
setAction(Action.Run)
setModalLoading(true)
let {data: command} = await smClient.getCommand(selectedProcess.name)
let {data: command} = await api.getCommand(selectedProcess.name)
setCommand(command)
setModalLoading(false)
}}
@ -299,14 +341,28 @@ export default function Processes() {
argumentsParams={selectedProcess.arguments}
callback={callback}/>}
</ConfirmDialog>}
{selectedProcess && <ConfirmDialog
title={selectedProcess.name}
open={Action.Approve === action}
agreeCallback={agreeCallback}
closeCallback={() => {setAction(null)}}>
Approve?
</ConfirmDialog>}
{selectedProcess && <ConfirmDialog
title={selectedProcess.name}
open={Action.Unlock === action}
agreeCallback={agreeCallback}
closeCallback={() => {setAction(null)}}>
Unlock?
</ConfirmDialog>}
{selectedProcess && <ConfirmDialog
title={`Are you sure?`}
open={ !!action && ([Action.Play, Action.Pause, Action.Stop, Action.Kill].indexOf(action) > -1) }
agreeCallback={async () => {
Action.Play === action && await smClient.playProcess(selectedProcess.id)
Action.Pause === action && await smClient.pauseProcess(selectedProcess.id)
Action.Stop === action && await smClient.stopProcess(selectedProcess.id)
Action.Kill === action && await smClient.killProcess(selectedProcess.id)
Action.Play === action && await api.playProcess(selectedProcess.id)
Action.Pause === action && await api.pauseProcess(selectedProcess.id)
Action.Stop === action && await api.stopProcess(selectedProcess.id)
Action.Kill === action && await api.killProcess(selectedProcess.id)
setAction(null)
}}
closeCallback={() => {setAction(null)}}>

View File

@ -0,0 +1,116 @@
import styles from './styles.module.css'
import {Autocomplete, IconButton, TextareaAutosize, TextField} from "@mui/material";
import {useContext, useEffect, useState} from "react";
import Context from "../../../context/token";
import AddCircleOutlineOutlinedIcon from '@mui/icons-material/AddCircleOutlineOutlined';
import DeleteForeverOutlined from '@mui/icons-material/DeleteForeverOutlined';
import Modal from '@mui/material/Modal';
import Fade from '@mui/material/Fade';
import Backdrop from '@mui/material/Backdrop';
import Box from '@mui/material/Box';
import Grid from "@mui/material/Grid";
import ConfirmDialog from "../confirm-dialog";
const style = {
position: 'absolute' as 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: '100%',
bgcolor: 'background.paper',
border: '2px solid #000',
boxShadow: 24,
p: 4,
};
export default function Tokens() {
const {token, tokens, addToken, deleteToken, switchToken} = useContext(Context)
const [showAddForm, setShowAddForm] = useState(false)
const [showConfirm, setShowConfirm] = useState(false)
let variants = tokens.map((token, index) => ({
...token,
label: `${token.host} [${token.id}]`
}));
useEffect(() => {
setShowAddForm(!token)
}, [token])
return (
<>
<Grid container>
<Grid item xs={2}>
<Autocomplete
freeSolo
value={{
...token,
label: token?.host ? `${token.host} [${token.id}]` : ''
}}
onChange={(event: any, newValue: any) => {
console.log('newValue', newValue);
if (newValue?.id) {
switchToken(newValue?.id)
}
}}
disablePortal
options={variants}
renderInput={(params) => <TextField {...params} label="Tokens"/>}
/>
</Grid>
<Grid item xs={1}>
<IconButton onClick={() => setShowAddForm(true)} title={`Add new token`}
aria-label="Add new token">
<AddCircleOutlineOutlinedIcon/>
</IconButton>
<IconButton onClick={() => setShowConfirm(true)} title={`Delete current token`}
aria-label="Add new token">
<DeleteForeverOutlined/>
</IconButton>
</Grid>
</Grid>
<Modal
aria-labelledby="transition-modal-title"
aria-describedby="transition-modal-description"
open={showAddForm}
onClose={() => setShowAddForm(false)}
closeAfterTransition
slots={{backdrop: Backdrop}}
slotProps={{
backdrop: {
timeout: 500,
},
}}
>
<Fade in={showAddForm}>
<Box sx={style}>
<TextareaAutosize
placeholder={`Pass JWT token`}
minRows={25}
style={{width: '100%'}}
onInput={(event: any) => {
if (addToken(event.target.value)) {
setShowAddForm(false)
}
}}
/>
</Box>
</Fade>
</Modal>
<ConfirmDialog
title={`Delete curent token`}
open={showConfirm}
agreeCallback={async () => {
let tokenId = token?.id || null
if (tokenId) {
deleteToken(tokenId)
}
setShowConfirm(false)
}}
closeCallback={() => {setShowConfirm(false)}}>
Confirm?
</ConfirmDialog>
</>
)
}

6
context/token.ts Normal file
View File

@ -0,0 +1,6 @@
import React from 'react'
import {UseTokenInterface} from "../hooks/use-token"
const Context = React.createContext({} as UseTokenInterface)
export const Provider = Context.Provider
export default Context

130
functions/token.ts Normal file
View File

@ -0,0 +1,130 @@
export interface TokenData<T=any> {
payload: T
headers: Record<string, any>
}
const isClient = () => {
return typeof window !== 'undefined'
}
export const storeToken = (token: string) => {
if (!isClient()) {
return;
}
let tokenData = grabTokenData(token)
let tokenId = tokenData?.payload?.id;
if (!tokenId) {
return;
}
let tokens: string | null = window.localStorage.getItem('tokens')
let tokensData: string[] = JSON.parse(tokens || '[]');
if (!tokensData) {
return
}
window.localStorage.setItem('token', tokenId)
window.localStorage.setItem(tokenId, token)
tokensData.push(tokenId)
window.localStorage.setItem('tokens', JSON.stringify(tokensData))
}
export const cleanToken = (id: string) => {
if (!isClient()) {
return;
}
let tokens: string | null = window.localStorage.getItem('tokens')
let tokensData: string[] = JSON.parse(tokens || '[]');
if (!tokensData) {
return
}
tokensData = tokensData.filter((storedTokenId: string) => storedTokenId !== id)
window.localStorage.setItem('tokens', JSON.stringify(tokensData))
window.localStorage.removeItem(id)
// Чистим текущий токен
let activeTokenId: string | null = window.localStorage.getItem('token')
if (id === activeTokenId) {
window.localStorage.removeItem('token')
}
}
export const selectToken = (id: string) => {
if (!isClient()) {
return;
}
window.localStorage.setItem('token', id)
}
export const grabTokenData = (token: string): TokenData|null => {
if (!isClient()) {
return null
}
const parts = token.split('.')
if (parts.length !== 3) {
return null
}
let payload = JSON.parse(window.atob(parts[1]))
if (!payload){
return null
}
let headers = JSON.parse(window.atob(parts[0]))
if (!headers){
return null
}
return {
payload,
headers,
};
}
export const grabRawToken = (): string | null => {
if (!isClient()) {
return null
}
const tokenId = window.localStorage.getItem('token')
if (!tokenId) {
return null
}
return window.localStorage.getItem(tokenId)
}
export const grabToken = (): TokenData | null => {
const token = grabRawToken()
if (!token) {
return null
}
return grabTokenData(token);
}
export const grabTokens = (): TokenData[] => {
if (!isClient()) {
return []
}
const storedTokens = window.localStorage.getItem('tokens')
let tokens: string[] = JSON.parse(storedTokens || '[]');
if (!tokens) {
return []
}
let result: TokenData[] = []
tokens.forEach((tokenId) => {
let token = window.localStorage.getItem(tokenId)
if (!token) {
return true
}
let tokenData = grabTokenData(token)
if (tokenData) {
result.push(tokenData)
}
})
return result
}

30
hooks/use-api.ts Normal file
View File

@ -0,0 +1,30 @@
import {useContext, useEffect, useState} from 'react'
import {SMClient} from "../api/sm/sm-client";
import Context from "../context/token";
import {Token} from "./use-token";
import {BearerAuthentication} from "../api/authentication";
import {grabRawToken} from "../functions/token";
const grabClient = (token: Token | null, rawToken: string | null): SMClient => {
let authentication = {}
if (rawToken) {
authentication = {
authentication: new BearerAuthentication(rawToken)
}
}
return new SMClient({
baseUrl: token?.host || 'localhost',
...authentication
})
}
export function useApi(): SMClient {
let {token} = useContext(Context)
let [client, setClient] = useState<SMClient>(grabClient(token, grabRawToken()))
useEffect(() => {
setClient(grabClient(token, grabRawToken()))
}, [token])
return client
}

57
hooks/use-token.ts Normal file
View File

@ -0,0 +1,57 @@
import {useState, useEffect} from 'react'
import {cleanToken, grabToken, grabTokenData, grabTokens, selectToken, storeToken} from "../functions/token";
export class Token {
id!: string
host!: string
permissions!: string[]
}
export interface UseTokenInterface {
token: Token|null
tokens: Token[]
switchToken(tokenId: string): void
deleteToken(tokenId: string): void
addToken(tokenId: string): boolean
}
export function useToken(): UseTokenInterface {
const [token, setToken] = useState<Token|null>(null);
const [tokens, setTokens] = useState<Token[]>([]);
useEffect(() => {
setToken(grabToken()?.payload)
let tokens = grabTokens().map((tokenData) => tokenData.payload as Token)
setTokens(tokens)
return () => {
}
}, [])
const switchToken = (tokenId: string) => {
selectToken(tokenId)
setToken(grabToken()?.payload)
}
const deleteToken = (tokenId: string) => {
cleanToken(tokenId)
setToken(grabToken()?.payload)
}
const addToken = (token: string): boolean => {
if (!grabTokenData(token)) {
return false;
}
storeToken(token)
setToken(grabToken()?.payload)
setTokens([
...tokens,
grabToken()?.payload
])
return true
}
return {token, tokens, switchToken, deleteToken, addToken}
}

View File

@ -1,6 +1,13 @@
import '../styles/globals.css'
import type { AppProps } from 'next/app'
import {Provider} from "../context/token";
import {useToken} from "../hooks/use-token";
export default function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
let {token, tokens, switchToken, deleteToken, addToken} = useToken()
return (
<Provider value={{token, tokens, switchToken, deleteToken, addToken}}>
<Component {...pageProps} />
</Provider>
)
}

View File

@ -2,9 +2,12 @@ import Head from 'next/head'
import styles from '../styles/Home.module.css'
import Processes from "../components/elements/processes";
import {Tabs, Tab} from '@mui/material';
import {useState} from "react";
import {useContext, useState} from "react";
import Commands from "../components/elements/commands";
import {Provider as TabProvider} from '../context/tab'
import Context from "../context/token";
import Tokens from "../components/elements/tokens";
export enum TabEnum {
Commands,
@ -12,8 +15,10 @@ export enum TabEnum {
}
export default function Home() {
let {token} = useContext(Context)
const [tab, setTab] = useState<TabEnum>(TabEnum.Processes);
const handleChange = (event: any, tab: number) => setTab(tab)
return (
<>
<Head>
@ -23,21 +28,24 @@ export default function Home() {
<link rel="icon" href="/favicon.ico"/>
</Head>
<main className={styles.main}>
<Tabs
value={tab}
onChange={handleChange}
variant="scrollable"
scrollButtons
allowScrollButtonsMobile
aria-label="scrollable force tabs example"
>
<Tab value={TabEnum.Commands} label="Commands" />
<Tab value={TabEnum.Processes} label="Processes" />
</Tabs>
<TabProvider value={{tab, setTab}}>
{tab === TabEnum.Commands && <Commands />}
{tab === TabEnum.Processes && <Processes />}
</TabProvider>
<Tokens />
{token && <>
<Tabs
value={tab}
onChange={handleChange}
variant="scrollable"
scrollButtons
allowScrollButtonsMobile
aria-label="scrollable force tabs example"
>
<Tab value={TabEnum.Commands} label="Commands" />
<Tab value={TabEnum.Processes} label="Processes" />
</Tabs>
<TabProvider value={{tab, setTab}}>
{tab === TabEnum.Commands && <Commands />}
{tab === TabEnum.Processes && <Processes />}
</TabProvider>
</>}
</main>
</>
)

View File

@ -1,4 +1,4 @@
<!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width"/><title>404: This page could not be found</title><meta name="next-head-count" content="3"/><link rel="preload" href="/_next/static/css/961153d3a9f88130.css" as="style"/><link rel="stylesheet" href="/_next/static/css/961153d3a9f88130.css" data-n-g=""/><noscript data-n-css=""></noscript><script defer="" nomodule="" src="/_next/static/chunks/polyfills-c67a75d1b6f99dc8.js"></script><script src="/_next/static/chunks/webpack-0b5d8249fb15f5f3.js" defer=""></script><script src="/_next/static/chunks/framework-114634acb84f8baa.js" defer=""></script><script src="/_next/static/chunks/main-66ca454f7bdf962f.js" defer=""></script><script src="/_next/static/chunks/pages/_app-891652dd44e1e4e1.js" defer=""></script><script src="/_next/static/chunks/pages/_error-8353112a01355ec2.js" defer=""></script><script src="/_next/static/J4k37v5qNO58v9nWL4ohq/_buildManifest.js" defer=""></script><script src="/_next/static/J4k37v5qNO58v9nWL4ohq/_ssgManifest.js" defer=""></script></head><body><div id="__next"><div style="font-family:-apple-system, BlinkMacSystemFont, Roboto, &quot;Segoe UI&quot;, &quot;Fira Sans&quot;, Avenir, &quot;Helvetica Neue&quot;, &quot;Lucida Grande&quot;, sans-serif;height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center"><div><style>
<!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width"/><title>404: This page could not be found</title><meta name="next-head-count" content="3"/><link rel="preload" href="/_next/static/css/961153d3a9f88130.css" as="style"/><link rel="stylesheet" href="/_next/static/css/961153d3a9f88130.css" data-n-g=""/><noscript data-n-css=""></noscript><script defer="" nomodule="" src="/_next/static/chunks/polyfills-c67a75d1b6f99dc8.js"></script><script src="/_next/static/chunks/webpack-0b5d8249fb15f5f3.js" defer=""></script><script src="/_next/static/chunks/framework-114634acb84f8baa.js" defer=""></script><script src="/_next/static/chunks/main-66ca454f7bdf962f.js" defer=""></script><script src="/_next/static/chunks/pages/_app-526701f4ebc801df.js" defer=""></script><script src="/_next/static/chunks/pages/_error-8353112a01355ec2.js" defer=""></script><script src="/_next/static/InkGKW3c-wb1BcW3qzzDZ/_buildManifest.js" defer=""></script><script src="/_next/static/InkGKW3c-wb1BcW3qzzDZ/_ssgManifest.js" defer=""></script></head><body><div id="__next"><div style="font-family:-apple-system, BlinkMacSystemFont, Roboto, &quot;Segoe UI&quot;, &quot;Fira Sans&quot;, Avenir, &quot;Helvetica Neue&quot;, &quot;Lucida Grande&quot;, sans-serif;height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center"><div><style>
body { margin: 0; color: #000; background: #fff; }
.next-error-h1 {
border-right: 1px solid rgba(0, 0, 0, .3);
@ -9,4 +9,4 @@
.next-error-h1 {
border-right: 1px solid rgba(255, 255, 255, .3);
}
}</style><h1 class="next-error-h1" style="display:inline-block;margin:0;margin-right:20px;padding:0 23px 0 0;font-size:24px;font-weight:500;vertical-align:top;line-height:49px">404</h1><div style="display:inline-block;text-align:left;line-height:49px;height:49px;vertical-align:middle"><h2 style="font-size:14px;font-weight:normal;line-height:49px;margin:0;padding:0">This page could not be found<!-- -->.</h2></div></div></div></div><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{"statusCode":404}},"page":"/_error","query":{},"buildId":"J4k37v5qNO58v9nWL4ohq","nextExport":true,"isFallback":false,"gip":true,"scriptLoader":[]}</script></body></html>
}</style><h1 class="next-error-h1" style="display:inline-block;margin:0;margin-right:20px;padding:0 23px 0 0;font-size:24px;font-weight:500;vertical-align:top;line-height:49px">404</h1><div style="display:inline-block;text-align:left;line-height:49px;height:49px;vertical-align:middle"><h2 style="font-size:14px;font-weight:normal;line-height:49px;margin:0;padding:0">This page could not be found<!-- -->.</h2></div></div></div></div><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{"statusCode":404}},"page":"/_error","query":{},"buildId":"InkGKW3c-wb1BcW3qzzDZ","nextExport":true,"isFallback":false,"gip":true,"scriptLoader":[]}</script></body></html>

View File

@ -0,0 +1 @@
self.__BUILD_MANIFEST={__rewrites:{beforeFiles:[],afterFiles:[],fallback:[]},"/":["static/chunks/317-7a9f5b1fcf3cfbe5.js","static/css/758d1a1ad3b56ec4.css","static/chunks/pages/index-e05dda18f04fa62a.js"],"/_error":["static/chunks/pages/_error-8353112a01355ec2.js"],sortedPages:["/","/_app","/_error"]},self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB();

View File

@ -1 +0,0 @@
self.__BUILD_MANIFEST={__rewrites:{beforeFiles:[],afterFiles:[],fallback:[]},"/":["static/chunks/556-56e85e4880bbe04b.js","static/css/758d1a1ad3b56ec4.css","static/chunks/pages/index-84dfafb8f3c7c7ce.js"],"/_error":["static/chunks/pages/_error-8353112a01355ec2.js"],sortedPages:["/","/_app","/_error"]},self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[888],{6840:function(t,e,n){(window.__NEXT_P=window.__NEXT_P||[]).push(["/_app",function(){return n(2595)}])},7889:function(t,e,n){"use strict";n.d(e,{z:function(){return l}});var r=n(7294);let o=r.createContext({}),l=o.Provider;e.Z=o},4085:function(t,e,n){"use strict";n.d(e,{DR:function(){return c},F8:function(){return d},Y0:function(){return l},gn:function(){return a},mB:function(){return o},rK:function(){return u},se:function(){return i}});let r=()=>!0,o=t=>{var e;if(!r())return;let n=i(t),o=null==n?void 0:null===(e=n.payload)||void 0===e?void 0:e.id;if(!o)return;let l=JSON.parse(window.localStorage.getItem("tokens")||"[]");l&&(window.localStorage.setItem("token",o),window.localStorage.setItem(o,t),l.push(o),window.localStorage.setItem("tokens",JSON.stringify(l)))},l=t=>{if(!r())return;let e=JSON.parse(window.localStorage.getItem("tokens")||"[]");e&&(e=e.filter(e=>e!==t),window.localStorage.setItem("tokens",JSON.stringify(e)),window.localStorage.removeItem(t),t===window.localStorage.getItem("token")&&window.localStorage.removeItem("token"))},u=t=>{r()&&window.localStorage.setItem("token",t)},i=t=>{if(!r())return null;let e=t.split(".");if(3!==e.length)return null;let n=JSON.parse(window.atob(e[1]));if(!n)return null;let o=JSON.parse(window.atob(e[0]));return o?{payload:n,headers:o}:null},a=()=>{if(!r())return null;let t=window.localStorage.getItem("token");return t?window.localStorage.getItem(t):null},d=()=>{let t=a();return t?i(t):null},c=()=>{if(!r())return[];let t=window.localStorage.getItem("tokens"),e=JSON.parse(t||"[]");if(!e)return[];let n=[];return e.forEach(t=>{let e=window.localStorage.getItem(t);if(!e)return!0;let r=i(e);r&&n.push(r)}),n}},2595:function(t,e,n){"use strict";n.r(e),n.d(e,{default:function(){return i}});var r=n(5893);n(4744);var o=n(7889),l=n(7294),u=n(4085);function i(t){let{Component:e,pageProps:n}=t,{token:i,tokens:a,switchToken:d,deleteToken:c,addToken:f}=function(){let[t,e]=(0,l.useState)(null),[n,r]=(0,l.useState)([]);(0,l.useEffect)(()=>{var t;return e(null===(t=(0,u.F8)())||void 0===t?void 0:t.payload),r((0,u.DR)().map(t=>t.payload)),()=>{}},[]);let o=t=>{var n;(0,u.rK)(t),e(null===(n=(0,u.F8)())||void 0===n?void 0:n.payload)},i=t=>{var n;(0,u.Y0)(t),e(null===(n=(0,u.F8)())||void 0===n?void 0:n.payload)},a=t=>{var o,l;return!!(0,u.se)(t)&&((0,u.mB)(t),e(null===(o=(0,u.F8)())||void 0===o?void 0:o.payload),r([...n,null===(l=(0,u.F8)())||void 0===l?void 0:l.payload]),!0)};return{token:t,tokens:n,switchToken:o,deleteToken:i,addToken:a}}();return(0,r.jsx)(o.z,{value:{token:i,tokens:a,switchToken:d,deleteToken:c,addToken:f},children:(0,r.jsx)(e,{...n})})}},4744:function(){}},function(t){var e=function(e){return t(t.s=e)};t.O(0,[774,179],function(){return e(6840),e(880)}),_N_E=t.O()}]);

View File

@ -1 +0,0 @@
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[888],{6840:function(n,u,t){(window.__NEXT_P=window.__NEXT_P||[]).push(["/_app",function(){return t(3847)}])},3847:function(n,u,t){"use strict";t.r(u),t.d(u,{default:function(){return _}});var r=t(5893);function _(n){let{Component:u,pageProps:t}=n;return(0,r.jsx)(u,{...t})}t(4744)},4744:function(){}},function(n){var u=function(u){return n(n.s=u)};n.O(0,[774,179],function(){return u(6840),u(880)}),_N_E=n.O()}]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long