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

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

Expand Down
23 changes: 23 additions & 0 deletions ui/src/message/Messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,22 @@ import ConfirmDialog from '../common/ConfirmDialog';
import LoadingSpinner from '../common/LoadingSpinner';
import {useStores} from '../stores';
import {Virtuoso} from 'react-virtuoso';
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 deleteMessage = (message: IMessage) => () => messagesStore.removeSingle(message);

Expand Down Expand Up @@ -93,6 +96,16 @@ const Messages = observer(() => {
title={name}
rightControl={
<div>
{app && (
<Button
id="push-message"
variant="contained"
color="primary"
onClick={() => setPushMessageOpen(true)}
style={{marginRight: 5}}>
Push Message
</Button>
)}
<Button
id="refresh-all"
variant="contained"
Expand Down Expand Up @@ -123,6 +136,16 @@ const Messages = observer(() => {
fOnSubmit={() => messagesStore.removeByApp(appId)}
/>
)}
{pushMessageOpen && app && (
<PushMessageDialog
appName={app.name}
defaultPriority={app.defaultPriority}
fClose={() => setPushMessageOpen(false)}
fOnSubmit={(message, title, priority) =>
messagesStore.sendMessage(app.id, message, title, priority)
}
/>
)}
</DefaultPage>
);
});
Expand Down
19 changes: 19 additions & 0 deletions ui/src/message/MessagesStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,25 @@ export class MessagesStore {
this.snack('Message deleted');
};

public sendMessage = async (
appId: number,
message: string,
title: string,
priority: number
): Promise<void> => {
const app = this.appStore.getByID(appId);
const payload: Pick<IMessage, 'title' | 'message' | 'priority'> = {
message,
priority,
title,
};

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

public clearAll = () => {
this.state = {};
this.createEmptyStatesForApps(this.appStore.getItems());
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>
);
};
30 changes: 29 additions & 1 deletion ui/src/tests/message.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
// 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,
waitToDisappear,
} 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 +97,26 @@ 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 waitToDisappear(page, '#push-message-dialog');
expect(await extractMessages(1)).toEqual([m('UI Test', 'Hello from UI')]);
await page.click('#messages .message .delete');
expect(await extractMessages(0)).toEqual([]);
await navigate('All Messages');
});

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