Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
15 changes: 14 additions & 1 deletion ui/src/apiAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,20 @@ import {SnackReporter} from './snack/SnackManager';

export const initAxios = (currentUser: CurrentUser, snack: SnackReporter) => {
axios.interceptors.request.use((config) => {
config.headers['X-Gotify-Key'] = currentUser.token();
const headers = config.headers ?? {};
const hasHeader = (key: string) => {
if (typeof headers.get === 'function') {
return headers.get(key) != null;
}
if (typeof headers.has === 'function') {
return headers.has(key);
}
return key in headers;
};
if (!hasHeader('X-Gotify-Key') && !hasHeader('x-gotify-key')) {
headers['X-Gotify-Key'] = currentUser.token();
}
config.headers = headers;
return config;
});

Expand Down
19 changes: 19 additions & 0 deletions ui/src/application/AppStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,25 @@ export class AppStore extends BaseStore<IApplication> {
this.snack('Application created');
};

public sendMessage = async (
id: number,
message: string,
title: string,
priority: number
): Promise<void> => {
const app = this.getByID(id);
const payload: {message: string; title?: string; priority?: number} = {message};
if (title.trim() !== '') {
payload.title = title;
}
payload.priority = priority;

await axios.post(`${config.get('url')}message`, payload, {
headers: {'X-Gotify-Key': app.token},
});
this.snack(`Message sent to ${app.name}`);
};

public getName = (id: number): string => {
const app = this.getByIDOrUndefined(id);
return id === -1 ? 'All Messages' : app !== undefined ? app.name : 'unknown';
Expand Down
29 changes: 29 additions & 0 deletions ui/src/message/Messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,24 @@ import ConfirmDialog from '../common/ConfirmDialog';
import LoadingSpinner from '../common/LoadingSpinner';
import {useStores} from '../stores';
import {Virtuoso} from 'react-virtuoso';
import Tooltip from '@mui/material/Tooltip';
import {PushMessageDialog} from './PushMessageDialog';

const Messages = observer(() => {
const {id} = useParams<{id: string}>();
const appId = id == null ? -1 : parseInt(id as string, 10);

const [deleteAll, setDeleteAll] = React.useState(false);
const [pushMessageOpen, setPushMessageOpen] = React.useState(false);
const [isLoadingMore, setLoadingMore] = React.useState(false);
const {messagesStore, appStore} = useStores();
const messages = messagesStore.get(appId);
const hasMore = messagesStore.canLoadMore(appId);
const name = appStore.getName(appId);
const hasMessages = messages.length !== 0;
const expandedState = React.useRef<Record<number, boolean>>({});
const app = appId === -1 ? undefined : appStore.getByIDOrUndefined(appId);
const canPushMessage = appId !== -1 && app !== undefined;

const deleteMessage = (message: IMessage) => () => messagesStore.removeSingle(message);

Expand Down Expand Up @@ -93,6 +98,20 @@ const Messages = observer(() => {
title={name}
rightControl={
<div>
{canPushMessage && (
<Tooltip title="Push message">
<span>
<Button
id="push-message"
variant="contained"
color="primary"
onClick={() => setPushMessageOpen(true)}
style={{marginRight: 5}}>
Push Message
</Button>
</span>
</Tooltip>
)}
<Button
id="refresh-all"
variant="contained"
Expand Down Expand Up @@ -123,6 +142,16 @@ const Messages = observer(() => {
fOnSubmit={() => messagesStore.removeByApp(appId)}
/>
)}
{pushMessageOpen && app && (
<PushMessageDialog
appName={app.name}
defaultPriority={app.defaultPriority}
fClose={() => setPushMessageOpen(false)}
fOnSubmit={(message, title, priority) =>
appStore.sendMessage(app.id, message, title, priority)
}
/>
)}
</DefaultPage>
);
});
Expand Down
89 changes: 89 additions & 0 deletions ui/src/message/PushMessageDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import Button from '@mui/material/Button';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';
import TextField from '@mui/material/TextField';
import Tooltip from '@mui/material/Tooltip';
import React, {useState} from 'react';
import {NumberField} from '../common/NumberField';

interface IProps {
appName: string;
defaultPriority: number;
fClose: VoidFunction;
fOnSubmit: (message: string, title: string, priority: number) => Promise<void>;
}

export const PushMessageDialog = ({appName, defaultPriority, fClose, fOnSubmit}: IProps) => {
const [title, setTitle] = useState('');
const [message, setMessage] = useState('');
const [priority, setPriority] = useState(defaultPriority);

const submitEnabled = message.trim().length !== 0;
const submitAndClose = async () => {
await fOnSubmit(message, title, priority);
fClose();
};

return (
<Dialog
open={true}
onClose={fClose}
aria-labelledby="push-message-title"
id="push-message-dialog">
<DialogTitle id="push-message-title">Push message</DialogTitle>
<DialogContent>
<DialogContentText>
Send a push message via {appName}. Leave the title empty to use the
application name.
</DialogContentText>
<TextField
margin="dense"
className="title"
label="Title"
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
fullWidth
/>
<TextField
autoFocus
margin="dense"
className="message"
label="Message *"
type="text"
value={message}
onChange={(e) => setMessage(e.target.value)}
fullWidth
multiline
minRows={4}
/>
<NumberField
margin="dense"
className="priority"
label="Priority"
value={priority}
onChange={(value) => setPriority(value)}
fullWidth
/>
</DialogContent>
<DialogActions>
<Button onClick={fClose}>Cancel</Button>
<Tooltip title={submitEnabled ? '' : 'message is required'}>
<div>
<Button
className="send"
disabled={!submitEnabled}
onClick={submitAndClose}
color="primary"
variant="contained">
Send
</Button>
</div>
</Tooltip>
</DialogActions>
</Dialog>
);
};
21 changes: 20 additions & 1 deletion ui/src/tests/message.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// todo before all tests jest start puppeteer
import {Page} from 'puppeteer';
import {newTest, GotifyTest} from './setup';
import {clickByText, count, innerText, waitForCount, waitForExists} from './utils';
import {clearField, clickByText, count, innerText, waitForCount, waitForExists} from './utils';
import {afterAll, beforeAll, describe, expect, it} from 'vitest';
import * as auth from './authentication';
import * as selector from './selector';
Expand Down Expand Up @@ -89,6 +89,25 @@ describe('Messages', () => {
expect(await count(page, '#messages .message')).toBe(0);
await navigate('All Messages');
});
it('hides push message on all messages', async () => {
await navigate('All Messages');
expect(await count(page, '#push-message')).toBe(0);
});
it('pushes a message via ui', async () => {
await navigate('Windows');
await page.waitForSelector('#push-message');
await page.click('#push-message');
await page.waitForSelector('#push-message-dialog');
await page.type('#push-message-dialog .title input', 'UI Test');
await page.type('#push-message-dialog .message textarea', 'Hello from UI');
await clearField(page, '#push-message-dialog .priority input');
await page.type('#push-message-dialog .priority input', '2');
await page.click('#push-message-dialog .send');
await waitForExists(page, '.message .content', 'Hello from UI');
await page.click('#messages .message .delete');
await waitForCount(page, '#messages .message', 0);
await navigate('All Messages');
});

const extractMessages = async (expectCount: number) => {
await waitForCount(page, '#messages .message', expectCount);
Expand Down
Loading