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

feat: Login form and admin update #1589

Merged
merged 10 commits into from
Oct 2, 2024
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
32 changes: 8 additions & 24 deletions dkron/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"net/http"
"sort"
"strconv"
"time"

"github.com/distribworks/dkron/v4/types"
"github.com/gin-contrib/cors"
Expand Down Expand Up @@ -48,23 +47,21 @@ func NewTransport(a *Agent, log *logrus.Entry) *HTTPTransport {

func (h *HTTPTransport) ServeHTTP() {
h.Engine = gin.Default()
h.Engine.Use(h.Options)

rootPath := h.Engine.Group("/")

rootPath.Use(cors.New(cors.Config{
AllowOrigins: []string{"*"},
AllowMethods: []string{"*"},
AllowHeaders: []string{"*"},
ExposeHeaders: []string{"*"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}))
config := cors.DefaultConfig()
config.AllowAllOrigins = true
config.AllowMethods = []string{"*"}
config.AllowHeaders = []string{"*"}
config.ExposeHeaders = []string{"*"}

rootPath.Use(cors.New(config))
rootPath.Use(h.MetaMiddleware())

h.APIRoutes(rootPath)
if h.agent.config.UI {
h.UI(rootPath, uiDist)
h.UI(rootPath, false)
}

h.logger.WithFields(logrus.Fields{
Expand Down Expand Up @@ -131,19 +128,6 @@ func (h *HTTPTransport) MetaMiddleware() gin.HandlerFunc {
}
}

func (h *HTTPTransport) Options(c *gin.Context) {
if c.Request.Method != "OPTIONS" {
c.Next()
} else {
c.Header("Allow", "HEAD,GET,POST,PUT,PATCH,DELETE,OPTIONS")
c.Header("Content-Type", "application/json")
gh := cors.Default()
gh(c)

c.AbortWithStatus(http.StatusOK)
}
}

func renderJSON(c *gin.Context, status int, v interface{}) {
if _, ok := c.GetQuery(pretty); ok {
c.IndentedJSON(status, v)
Expand Down
235 changes: 235 additions & 0 deletions dkron/ui-dist/assets/index-244e3810.js

Large diffs are not rendered by default.

240 changes: 0 additions & 240 deletions dkron/ui-dist/assets/index-3ed3d739.js

This file was deleted.

17 changes: 9 additions & 8 deletions dkron/ui-dist/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<meta name="theme-color" content="#000000" />
<link rel="manifest" href="./manifest.json" />
<link rel="shortcut icon" href="./favicon.ico" />
<title>ui</title>
<title>Dkron</title>
<style>
body {
margin: 0;
Expand Down Expand Up @@ -112,13 +112,14 @@
rel="stylesheet"
/>
<script>window.global = window;</script>
<script>window.DKRON_API_URL = {{.DKRON_API_URL }};
window.DKRON_LEADER = {{.DKRON_LEADER }};
window.DKRON_TOTAL_JOBS = {{.DKRON_TOTAL_JOBS }};
window.DKRON_SUCCESSFUL_JOBS = {{.DKRON_SUCCESSFUL_JOBS }};
window.DKRON_FAILED_JOBS = {{.DKRON_FAILED_JOBS }};
window.DKRON_UNTRIGGERED_JOBS = {{.DKRON_UNTRIGGERED_JOBS }};</script>
<script type="module" crossorigin src="./assets/index-3ed3d739.js"></script>
<script>window.DKRON_API_URL = {{.DKRON_API_URL}};
window.DKRON_LEADER = {{.DKRON_LEADER}};
window.DKRON_TOTAL_JOBS = {{.DKRON_TOTAL_JOBS}};
window.DKRON_SUCCESSFUL_JOBS = {{.DKRON_SUCCESSFUL_JOBS}};
window.DKRON_FAILED_JOBS = {{.DKRON_FAILED_JOBS}};
window.DKRON_UNTRIGGERED_JOBS = {{.DKRON_UNTRIGGERED_JOBS}};
window.DKRON_ACL_ENABLED = {{.DKRON_ACL_ENABLED}};</script>
<script type="module" crossorigin src="./assets/index-244e3810.js"></script>
<link rel="stylesheet" href="./assets/index-73a69410.css">
</head>

Expand Down
5 changes: 3 additions & 2 deletions dkron/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const uiPathPrefix = "ui/"
var uiDist embed.FS

// UI registers UI specific routes on the gin RouterGroup.
func (h *HTTPTransport) UI(r *gin.RouterGroup, uifs embed.FS) {
func (h *HTTPTransport) UI(r *gin.RouterGroup, aclEnabled bool) {
// If we are visiting from a browser redirect to the dashboard
r.GET("/", func(c *gin.Context) {
switch c.NegotiateFormat(gin.MIMEHTML) {
Expand All @@ -31,7 +31,7 @@ func (h *HTTPTransport) UI(r *gin.RouterGroup, uifs embed.FS) {

ui := r.Group("/" + uiPathPrefix)

assets, err := fs.Sub(uifs, "ui-dist")
assets, err := fs.Sub(uiDist, "ui-dist")
if err != nil {
h.logger.Fatal(err)
}
Expand Down Expand Up @@ -87,6 +87,7 @@ func (h *HTTPTransport) UI(r *gin.RouterGroup, uifs embed.FS) {
"DKRON_FAILED_JOBS": failedJobs,
"DKRON_UNTRIGGERED_JOBS": untriggeredJobs,
"DKRON_SUCCESSFUL_JOBS": successfulJobs,
"DKRON_ACL_ENABLED": aclEnabled,
})
}
})
Expand Down
Binary file removed ui/bun.lockb
Binary file not shown.
15 changes: 8 additions & 7 deletions ui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<meta name="theme-color" content="#000000" />
<link rel="manifest" href="./manifest.json" />
<link rel="shortcut icon" href="./favicon.ico" />
<title>ui</title>
<title>Dkron</title>
<style>
body {
margin: 0;
Expand Down Expand Up @@ -112,12 +112,13 @@
rel="stylesheet"
/>
<script>window.global = window;</script>
<script>window.DKRON_API_URL = {{.DKRON_API_URL }};
window.DKRON_LEADER = {{.DKRON_LEADER }};
window.DKRON_TOTAL_JOBS = {{.DKRON_TOTAL_JOBS }};
window.DKRON_SUCCESSFUL_JOBS = {{.DKRON_SUCCESSFUL_JOBS }};
window.DKRON_FAILED_JOBS = {{.DKRON_FAILED_JOBS }};
window.DKRON_UNTRIGGERED_JOBS = {{.DKRON_UNTRIGGERED_JOBS }};</script>
<script>window.DKRON_API_URL = {{.DKRON_API_URL}};
window.DKRON_LEADER = {{.DKRON_LEADER}};
window.DKRON_TOTAL_JOBS = {{.DKRON_TOTAL_JOBS}};
window.DKRON_SUCCESSFUL_JOBS = {{.DKRON_SUCCESSFUL_JOBS}};
window.DKRON_FAILED_JOBS = {{.DKRON_FAILED_JOBS}};
window.DKRON_UNTRIGGERED_JOBS = {{.DKRON_UNTRIGGERED_JOBS}};
window.DKRON_ACL_ENABLED = {{.DKRON_ACL_ENABLED}};</script>
</head>

<body>
Expand Down
10 changes: 8 additions & 2 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,18 @@
"@mui/icons-material": "^5.14.19",
"@mui/material": "^5.14.20",
"@mui/styles": "^5.14.20",
"history": "^5.1.0",
"ra-core": "^4.0.0",
"ra-data-json-server": "^4.16.2",
"ra-data-simple-rest": "^4.16.0",
"react": "^18.2.0",
"react-admin": "^4.16.0",
"react": "^18.0.0",
"react-admin": "^5.2.1",
"react-admin-json-view": "^2.0.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.43.9",
"react-is": "^18.0.0",
"react-router": "^6.1.0",
"react-router-dom": "^6.1.0",
"recharts": "^2.11.0"
},
"devDependencies": {
Expand Down
10 changes: 7 additions & 3 deletions ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import jobs from './jobs';
import { BusyList } from './executions/BusyList';
import { Layout } from './layout';
import dataProvider from './dataProvider';
import authProvider from './authProvider';
import Dashboard from './dashboard';
import Settings from './settings/Settings';
import LoginPage from './LoginPage';

declare global {
interface Window {
Expand All @@ -18,18 +20,20 @@ declare global {
DKRON_FAILED_JOBS: string;
DKRON_SUCCESSFUL_JOBS: string;
DKRON_TOTAL_JOBS: string;
DKRON_ACL_ENABLED: boolean;
}
}

const authProvider = () => Promise.resolve();
const history = createHashHistory();

export const App = () => <Admin
dashboard={Dashboard}
authProvider={authProvider}
loginPage={LoginPage}
authProvider={window.DKRON_ACL_ENABLED ? authProvider : undefined}
dataProvider={dataProvider}
layout={Layout}
>

<Resource name="jobs" {...jobs} />
<Resource name="busy" options={{ label: 'Busy' }} list={BusyList} icon={PlayCircleOutlineIcon} />
<Resource name="executions" />
Expand Down
155 changes: 155 additions & 0 deletions ui/src/LoginPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { HtmlHTMLAttributes, ReactNode } from 'react';
import { useState } from 'react';
import {
Form,
useLogin,
useNotify,
TextInput,
useSafeSetState,
} from 'react-admin';
import { styled } from '@mui/material/styles';
import {
Button,
CardContent,
CircularProgress,
Avatar,
Card,
SxProps,
} from '@mui/material';
import LockIcon from '@mui/icons-material/Lock';

const LoginPage = (props: LoginFormProps) => {
const [token, setToken] = useState('');
const login = useLogin();
const notify = useNotify();
const avatarIcon = <LockIcon />;
const { className } = props;
const [loading, setLoading] = useSafeSetState(false);

const handleSubmit = (values: FormData) => {
setLoading(true);
login({ token }).catch(() => {
setLoading(false);
notify('Invalid token');
});
};

return (
<Root>
<Card className={LoginClasses.card}>
<div className={LoginClasses.avatar}>
<Avatar className={LoginClasses.icon}>{avatarIcon}</Avatar>
</div>
<StyledForm
onSubmit={handleSubmit}
mode="onChange"
noValidate
className={className}
>
<CardContent className={LoginFormClasses.content}>
<TextInput
name="token"
type="text"
value={token}
onChange={e => setToken(e.target.value)}
/>

<Button
variant="contained"
type="submit"
color="primary"
disabled={loading}
fullWidth
className={LoginFormClasses.button}
>
{loading ? (
<CircularProgress
className={LoginFormClasses.icon}
size={19}
thickness={3}
/>
) : (
"Sign in"
)}
</Button>
</CardContent>
</StyledForm>
</Card>
</Root>
);
};

export default LoginPage;

export interface LoginProps extends HtmlHTMLAttributes<HTMLDivElement> {
avatarIcon?: ReactNode;
backgroundImage?: string;
children?: ReactNode;
className?: string;
sx?: SxProps;
}

const PREFIX = 'RaLogin';
export const LoginClasses = {
card: `${PREFIX}-card`,
avatar: `${PREFIX}-avatar`,
icon: `${PREFIX}-icon`,
};

const Root = styled('div', {
name: PREFIX,
overridesResolver: (props, styles) => styles.root,
})(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
minHeight: '100vh',
height: '1px',
alignItems: 'center',
justifyContent: 'flex-start',
backgroundRepeat: 'no-repeat',
backgroundSize: 'cover',
backgroundImage:
'radial-gradient(circle at 50% 14em, #313264 0%, #00023b 60%, #00023b 100%)',

[`& .${LoginClasses.card}`]: {
minWidth: 300,
marginTop: '6em',
},
[`& .${LoginClasses.avatar}`]: {
margin: '1em',
display: 'flex',
justifyContent: 'center',
},
[`& .${LoginClasses.icon}`]: {
backgroundColor: theme.palette.secondary[500],
},
}));

const PREFIXF = 'RaLoginForm';

export const LoginFormClasses = {
content: `${PREFIXF}-content`,
button: `${PREFIXF}-button`,
icon: `${PREFIXF
}-icon`,
};

const StyledForm = styled(Form, {
name: PREFIXF,
overridesResolver: (props, styles) => styles.root,
})(({ theme }) => ({
[`& .${LoginFormClasses.content}`]: {
width: 300,
},
[`& .${LoginFormClasses.button}`]: {
marginTop: theme.spacing(2),
},
[`& .${LoginFormClasses.icon}`]: {
margin: theme.spacing(0.3),
},
}));

export interface LoginFormProps {
redirectTo?: string;
className?: string;
}
27 changes: 27 additions & 0 deletions ui/src/authProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { AuthProvider } from 'react-admin';

const authProvider: AuthProvider = {
login: ({ token }) => {
localStorage.setItem('token', token);
return Promise.resolve();
},
logout: () => {
localStorage.removeItem('token');
return Promise.resolve();
},
checkAuth: () =>
localStorage.getItem('token') ? Promise.resolve() : Promise.reject(),
checkError: (error) => {
const status = error.status;
if (status === 401 || status === 403) {
localStorage.removeItem('token');
return Promise.reject();
}
// other error code (404, 500, etc): no need to log out
return Promise.resolve();
},
getIdentity: () => Promise.resolve(),
getPermissions: () => Promise.resolve(),
};

export default authProvider;
Loading
Loading