Compare commits

...

2 commits

Author SHA1 Message Date
a3ac22d694 Add devcontainer configuration
All checks were successful
forgejo/Procyon/procyon/pipeline/pr-develop This commit looks good
forgejo/Procyon/procyon/pipeline/head This commit looks good
2023-11-09 10:31:10 +01:00
b4ebc22edd Update auth hooks + minor updates
Some checks are pending
forgejo/Procyon/procyon/pipeline/head Build started...
forgejo/Procyon/procyon/pipeline/pr-develop This commit looks good
2023-11-09 10:29:09 +01:00
25 changed files with 124 additions and 68 deletions

1
.devcontainer/Dockerfile Normal file
View file

@ -0,0 +1 @@
FROM local/nodejs-dev:n18_j11

View file

@ -0,0 +1,6 @@
{
"name": "procyon",
"workspaceFolder": "/home/project/procyon",
"dockerComposeFile": "docker-compose.yml",
"service": "procyon",
}

View file

@ -0,0 +1,12 @@
version: "3"
name: procyon
services:
procyon:
build:
context: .
container_name: procyon
volumes:
- ../:/home/project/procyon
command: sleep infinity

View file

@ -1,6 +1,6 @@
import { isJSON } from '@procyon/utils'; import { isJSON } from '@procyon/utils';
import { FetchTokensParams } from './types'; import { UseFetchTokensParams } from './types';
export const fetchTokens = async ({ export const fetchTokens = async ({
baseUrl, baseUrl,
@ -9,7 +9,7 @@ export const fetchTokens = async ({
code, code,
grantType, grantType,
redirectUri, redirectUri,
}: FetchTokensParams) => { }: UseFetchTokensParams) => {
try { try {
const response = await fetch(`${baseUrl}${tokenEndpoint}`, { const response = await fetch(`${baseUrl}${tokenEndpoint}`, {
method: 'post', method: 'post',

View file

@ -0,0 +1,8 @@
import { useSelector } from 'react-redux';
import { AUTH_REDUCER_NAME, AuthRootState } from '@procyon/auth/types';
import { ROOT_REDUCER_NAME } from '@procyon/constants/redux';
export const useAuthAttributes = () => {
return useSelector<AuthRootState>((state) => state[ROOT_REDUCER_NAME][AUTH_REDUCER_NAME]);
};

View file

@ -5,9 +5,9 @@ import { useLocation } from 'wouter';
import { setAuthTokens } from '@procyon/auth/slice'; import { setAuthTokens } from '@procyon/auth/slice';
import { fetchTokens } from '../actions'; import { fetchTokens } from '../actions';
import { UseAuthParams } from '../types'; import { UseLoginParams } from '../types';
export const useAuthCode = ({ baseUrl, clientId, tokenEndpoint, redirectUri }: UseAuthParams) => { export const useFetchTokens = ({ baseUrl, clientId, tokenEndpoint, redirectUri }: UseLoginParams) => {
const dispatch = useDispatch<any>(); const dispatch = useDispatch<any>();
const [, setLocation] = useLocation(); const [, setLocation] = useLocation();

View file

@ -1,6 +1,6 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { UseAuthParams } from '../types'; import { UseLoginParams } from '../types';
const generateState = () => const generateState = () =>
'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
@ -9,7 +9,7 @@ const generateState = () =>
return v.toString(16); return v.toString(16);
}); });
export const useLogin = ({ baseUrl, clientId, authEndpoint, redirectUri }: UseAuthParams) => { export const useLogin = ({ baseUrl, clientId, authEndpoint, redirectUri }: UseLoginParams) => {
return useCallback(() => { return useCallback(() => {
const state = generateState(); const state = generateState();
const urlParams: Record<string, string> = { const urlParams: Record<string, string> = {

View file

@ -1,7 +1,7 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { addSeconds } from 'date-fns'; import { addSeconds } from 'date-fns';
import { AUTH_REDUCER_NAME, AuthAttributes, AuthRootState, AuthTokenPayload, ModifyAuthAttribute } from './types'; import { AUTH_REDUCER_NAME, AuthAttributes, AuthRootState, AuthTokens, ModifyAuthAttributePayload } from './types';
const initialState: AuthAttributes = { const initialState: AuthAttributes = {
authenticated: undefined, authenticated: undefined,
@ -19,7 +19,7 @@ const authSlice = createSlice({
name: AUTH_REDUCER_NAME, name: AUTH_REDUCER_NAME,
initialState, initialState,
reducers: { reducers: {
setAuthTokens: (_, action: PayloadAction<AuthTokenPayload>) => { setAuthTokens: (_, action: PayloadAction<AuthTokens>) => {
const { accessExpiresIn, refreshExpiresIn } = action.payload; const { accessExpiresIn, refreshExpiresIn } = action.payload;
return { return {
...action.payload, ...action.payload,
@ -34,7 +34,7 @@ const authSlice = createSlice({
...action.payload, ...action.payload,
}; };
}, },
updateAuthAttribute: (state, action: PayloadAction<ModifyAuthAttribute>) => { updateAuthAttribute: (state, action: PayloadAction<ModifyAuthAttributePayload>) => {
state[action.payload.key] = action.payload.value; state[action.payload.key] = action.payload.value;
}, },
deleteAuthAttributes: () => { deleteAuthAttributes: () => {
@ -50,9 +50,10 @@ export const setAuthAttribute = authSlice.actions.setAuthAttribute as <T>(
) => PayloadAction<AuthAttributes<T>>; ) => PayloadAction<AuthAttributes<T>>;
export const updateAuthAttribute = authSlice.actions.updateAuthAttribute as <T>( export const updateAuthAttribute = authSlice.actions.updateAuthAttribute as <T>(
attrs: ModifyAuthAttribute<T> attrs: ModifyAuthAttributePayload<T>
) => PayloadAction<ModifyAuthAttribute<T>>; ) => PayloadAction<ModifyAuthAttributePayload<T>>;
export const getAuthAttribute = (state: AuthRootState, name: keyof AuthAttributes) => state.procyon.auth[name]; export const getAuthAttribute = <T>(state: AuthRootState, name: keyof AuthAttributes<T>) =>
state.procyon.auth[name as any];
export default authSlice.reducer; export default authSlice.reducer;

View file

@ -2,7 +2,7 @@ import { ROOT_REDUCER_NAME } from '@procyon/constants/redux';
export const AUTH_REDUCER_NAME = 'auth'; export const AUTH_REDUCER_NAME = 'auth';
export type AuthTokenPayload = { export type AuthTokens = {
accessExpiresIn?: number; accessExpiresIn?: number;
accessToken?: string; accessToken?: string;
idToken?: string; idToken?: string;
@ -11,14 +11,14 @@ export type AuthTokenPayload = {
sessionState?: string; sessionState?: string;
}; };
export type AuthAttributes<CustomAttributes = unknown> = { export type AuthAttributes<C = unknown> = {
accessExpiresTime?: string; accessExpiresTime?: string;
authenticated?: boolean; authenticated?: boolean;
refreshExpiresTime?: string; refreshExpiresTime?: string;
} & CustomAttributes & } & C &
AuthTokenPayload; AuthTokens;
export type ModifyAuthAttribute<P = unknown> = { export type ModifyAuthAttributePayload<P = unknown> = {
key: keyof AuthAttributes<P>; key: keyof AuthAttributes<P>;
value: any; value: any;
}; };
@ -29,7 +29,7 @@ export type AuthRootState = {
}; };
}; };
export type UseAuthParams = { export type UseLoginParams = {
authEndpoint: string; authEndpoint: string;
baseUrl: string; baseUrl: string;
clientId: string; clientId: string;
@ -38,7 +38,7 @@ export type UseAuthParams = {
tokenEndpoint: string; tokenEndpoint: string;
}; };
export type FetchTokensParams = Pick<UseAuthParams, 'baseUrl' | 'tokenEndpoint' | 'clientId'> & { export type UseFetchTokensParams = Pick<UseLoginParams, 'baseUrl' | 'tokenEndpoint' | 'clientId'> & {
code: string; code: string;
grantType: 'authorization_code'; grantType: 'authorization_code';
redirectUri: string; redirectUri: string;

View file

@ -69,7 +69,8 @@ export const DatePicker: React.FC<DatePickerProps<Date>> = (props) => {
label={label} label={label}
value={value ? formatFn(value, props.format ?? DefaultDateFormat) : undefined} value={value ? formatFn(value, props.format ?? DefaultDateFormat) : undefined}
{...omit(['value', 'onClick'], others)} {...omit(['value', 'onClick'], others)}
leftIcon={<TodayIcon onClick={handleOpenModal} className="cursor-pointer" />} leftIcon={<TodayIcon onClick={handleOpenModal} className="cursor-pointer text-primary-200" />}
placeholder={format ?? DefaultDateFormat}
/> />
); );
}; };

View file

@ -0,0 +1,10 @@
// icon:button-cursor | Material Design Icons https://materialdesignicons.com/ | Austin Andrews
import * as React from 'react';
export function ButtonCursorIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 24 24" fill="currentColor" height="1em" width="1em" {...props}>
<path d="M18.1 15.3c-.1.1-.3.2-.4.3l-2.4.4 1.7 3.6c.2.4 0 .8-.4 1l-2.8 1.3c-.1.1-.2.1-.3.1-.3 0-.6-.2-.7-.4L11.2 18l-1.9 1.5c-.1.1-.3.2-.5.2-.4 0-.8-.3-.8-.8V7.5c0-.5.3-.8.8-.8.2 0 .4.1.5.2l8.7 7.4c.3.2.4.7.1 1M6 12H4V4h16v8h-1.6l2.2 1.9c.8-.3 1.3-1 1.3-1.9V4c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v8c0 1.1.9 2 2 2h2v-2z" />
</svg>
);
}

View file

@ -3,22 +3,8 @@ import * as React from 'react';
export function ClockTimeIcon(props: React.SVGProps<SVGSVGElement>) { export function ClockTimeIcon(props: React.SVGProps<SVGSVGElement>) {
return ( return (
<svg viewBox="0 0 512 512" fill="currentColor" height="1em" width="1em" {...props}> <svg viewBox="0 0 24 24" fill="currentColor" height="1em" width="1em" {...props}>
<path <path d="M12 20c4.4 0 8-3.6 8-8s-3.6-8-8-8-8 3.6-8 8 3.6 8 8 8m0-18c5.5 0 10 4.5 10 10s-4.5 10-10 10S2 17.5 2 12 6.5 2 12 2m3.3 14.2L14 17l-3-5.2V7h1.5v4.4l2.8 4.8z" />
fill="none"
stroke="currentColor"
strokeMiterlimit={10}
strokeWidth={32}
d="M256 64C150 64 64 150 64 256s86 192 192 192 192-86 192-192S362 64 256 64z"
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={32}
d="M256 128v144h96"
/>
</svg> </svg>
); );
} }

View file

@ -0,0 +1,20 @@
// icon:pointer | Lucide https://lucide.dev/ | Lucide
import * as React from 'react';
export function PointerIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
viewBox="0 0 24 24"
height="1em"
width="1em"
{...props}>
<path d="M22 14a8 8 0 01-8 8M18 11v-1a2 2 0 00-2-2v0a2 2 0 00-2 2v0M14 10V9a2 2 0 00-2-2v0a2 2 0 00-2 2v1M10 9.5V4a2 2 0 00-2-2v0a2 2 0 00-2 2v10" />
<path d="M18 11a2 2 0 114 0v3a8 8 0 01-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 012.83-2.82L7 15" />
</svg>
);
}

View file

@ -122,7 +122,7 @@ const List: React.FC<IProps> = ({
<div <div
key={option.code} key={option.code}
id={`list-${name}_${i}`} id={`list-${name}_${i}`}
className={clsx('list', className, { className={clsx('list', {
'list-compact': compact, 'list-compact': compact,
'list--focus': index === i, 'list--focus': index === i,
'list--active': selectedItemCode === option.code, 'list--active': selectedItemCode === option.code,
@ -146,7 +146,7 @@ const List: React.FC<IProps> = ({
<TextField name="search" label="Hledat" onChange={handleSearch} autoFocus /> <TextField name="search" label="Hledat" onChange={handleSearch} autoFocus />
</div> </div>
)} )}
<div className={clsx('list-container')} onMouseLeave={setCurrentOption(-1)}> <div className={clsx('list-container', className)} onMouseLeave={setCurrentOption(-1)}>
{items} {items}
</div> </div>
</div> </div>

View file

@ -1,6 +1,7 @@
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { find, propEq, propOr } from 'ramda'; import { find, propEq, propOr } from 'ramda';
import { PointerIcon } from '@procyon/components/Icons/Pointer';
import { ModalId } from '@procyon/components/Modal/context'; import { ModalId } from '@procyon/components/Modal/context';
import { useModal } from '@procyon/components/Modal/hooks'; import { useModal } from '@procyon/components/Modal/hooks';
import { InputElementType } from '@procyon/types/form'; import { InputElementType } from '@procyon/types/form';
@ -14,12 +15,11 @@ export type SelectBoxProps<V = Option> = Omit<
autoComplete?: boolean; autoComplete?: boolean;
options: Option[]; options: Option[];
} & InputElementType<V>, } & InputElementType<V>,
'onFocus' | 'onClick' 'onFocus' | 'onClick' | 'onBlur'
>; >;
export const Selectbox: React.FC<SelectBoxProps> = (props) => { export const Selectbox: React.FC<SelectBoxProps> = (props) => {
const { value, options, onBlur, onChange, name, label, autoComplete, ...others } = const { value, options, onChange, name, label, autoComplete, ...others } = props;
props;
const { openModal, closeModal, registerModal } = useModal(); const { openModal, closeModal, registerModal } = useModal();
@ -32,10 +32,6 @@ export const Selectbox: React.FC<SelectBoxProps> = (props) => {
} }
}, [value, options]); }, [value, options]);
const handleBlur = (_: string, e: React.FocusEvent<HTMLInputElement>): void => {
onBlur?.(selectedOption ?? undefined, e);
};
const handleOptionClick = (option: Option): void => { const handleOptionClick = (option: Option): void => {
setSelectedOption(option); setSelectedOption(option);
onChange?.(option, null); onChange?.(option, null);
@ -81,11 +77,9 @@ export const Selectbox: React.FC<SelectBoxProps> = (props) => {
{...others} {...others}
name={name} name={name}
label={label} label={label}
onClick={handleOpenModal}
onFocus={handleOpenModal}
onBlur={handleBlur}
value={propOr('', 'name', selectedOption)} value={propOr('', 'name', selectedOption)}
readonly readonly
leftIcon={<PointerIcon className="cursor-pointer text-primary-200" onClick={handleOpenModal} />}
/> />
); );
}; };

View file

@ -29,6 +29,7 @@ export const TextField: React.FC<TextFieldProps> = (props) => {
status, status,
autoFocus, autoFocus,
mask, mask,
placeholder,
multiline, multiline,
leftIcon, leftIcon,
rightIcon, rightIcon,
@ -139,6 +140,7 @@ export const TextField: React.FC<TextFieldProps> = (props) => {
className={clsx(passProps.className, { className={clsx(passProps.className, {
'field-input--nolabel': !label, 'field-input--nolabel': !label,
})} })}
placeholder={placeholder}
autoComplete="off" autoComplete="off"
type={type} type={type}
/> />

View file

@ -42,6 +42,7 @@ const TimeList: React.FC<TimeListProps> = ({ title, onChange, min, max, step, se
selectedCode={selectedCode} selectedCode={selectedCode}
scrollToCode={scrollToCode} scrollToCode={scrollToCode}
enableAutoScroll enableAutoScroll
className="h-60"
/> />
</> </>
); );

View file

@ -46,7 +46,7 @@ export const TimePicker: FC<TimePickerProps<Date>> = (props) => {
registerModal({ registerModal({
id: name, id: name,
title: label, title: label,
style: { width: 250 }, style: { width: 200 },
Component: modalComponent, Component: modalComponent,
}); });
openModal(name); openModal(name);
@ -73,7 +73,9 @@ export const TimePicker: FC<TimePickerProps<Date>> = (props) => {
name={name} name={name}
label={label} label={label}
value={value ? format(value, 'HH:mm') : undefined} value={value ? format(value, 'HH:mm') : undefined}
leftIcon={<ClockTimeIcon onClick={handleOpenModal} className="cursor-pointer" />} leftIcon={<ClockTimeIcon onClick={handleOpenModal} className="cursor-pointer text-primary-200" />}
placeholder="HH:mm"
mask="99:99"
/> />
); );
}; };

View file

@ -6,7 +6,7 @@ import { Grid, GridSizeEnum } from '@procyon/components/Grid';
import { GridCol } from '@procyon/components/GridCol'; import { GridCol } from '@procyon/components/GridCol';
import TrashIcon from '@procyon/components/Icons/Trash'; import TrashIcon from '@procyon/components/Icons/Trash';
import { useModal } from '@procyon/components/Modal/hooks'; import { useModal } from '@procyon/components/Modal/hooks';
import { StatusEnum } from '@procyon/types/common'; import { SizeEnum, StatusEnum } from '@procyon/types/common';
import { Option } from '@procyon/types/options'; import { Option } from '@procyon/types/options';
import TimeList from './TimeList'; import TimeList from './TimeList';
@ -99,10 +99,16 @@ export const TimePickerModal: React.FC<TimePickerModalProps> = (props) => {
</Grid> </Grid>
<Grid cols={{ [base]: 2 }} className="mt-4"> <Grid cols={{ [base]: 2 }} className="mt-4">
<GridCol> <GridCol>
<Button label="Použít" status={StatusEnum.success} onClick={handleChangeValue} /> <Button size={SizeEnum.sm} label="Použít" status={StatusEnum.success} onClick={handleChangeValue} />
</GridCol> </GridCol>
<GridCol> <GridCol>
<Button label={<TrashIcon fontSize={20} />} status={StatusEnum.error} blank onClick={handleClearValue} /> <Button
label={<TrashIcon fontSize={20} />}
size={SizeEnum.sm}
status={StatusEnum.error}
blank
onClick={handleClearValue}
/>
</GridCol> </GridCol>
</Grid> </Grid>
</> </>

View file

@ -48,7 +48,6 @@ export const SelectBoxField: FC<SelectBoxFieldProps> = (props) => {
name={field.name} name={field.name}
value={field.value} value={field.value}
onChange={handleChange} onChange={handleChange}
onBlur={field.onBlur}
{...(error ? { status: StatusEnum.error } : {})} {...(error ? { status: StatusEnum.error } : {})}
/> />
<div className="field-validationMessage">{error?.message}</div> <div className="field-validationMessage">{error?.message}</div>

View file

@ -43,7 +43,7 @@ const field = (theme) => ({
}, },
'&-input': { '&-input': {
backgroundColor: 'white', backgroundColor: 'white',
fontSize: '18px', fontSize: '16px',
outline: 'none', outline: 'none',
cursor: 'text', cursor: 'text',
width: 'calc(100% - 0.75rem)', width: 'calc(100% - 0.75rem)',
@ -57,7 +57,7 @@ const field = (theme) => ({
}, },
'.checkbox-container, .radiobutton-container': { '.checkbox-container, .radiobutton-container': {
'.field-checkbox, .field-radio': { '.field-checkbox, .field-radio': {
fontSize: '18px', fontSize: '16px',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
cursor: 'pointer', cursor: 'pointer',

View file

@ -10,6 +10,7 @@ export type InputElementType<Value> = {
onChange?: (value: Value | undefined, e: React.MouseEvent | React.ChangeEvent | null) => void; onChange?: (value: Value | undefined, e: React.MouseEvent | React.ChangeEvent | null) => void;
onClick?: (value: Value, e: React.MouseEvent) => void; onClick?: (value: Value, e: React.MouseEvent) => void;
onFocus?: (value: Value, e: React.FocusEvent) => void; onFocus?: (value: Value, e: React.FocusEvent) => void;
placeholder?: string;
status?: StatusEnum; status?: StatusEnum;
value?: Value; value?: Value;
}; };

View file

@ -12,7 +12,7 @@ import hookCode from './helpers/hook?raw';
`@procyon/auth` module for authenticated. `@procyon/auth` module for authenticated.
For work with IdP (Identity Provider) server use `useLogin` and `useAuthCode` hooks. For work with IdP (Identity Provider) server use `useLogin` and `useFetchTokens` hooks.
<Unstyled> <Unstyled>
<Message status={StatusEnum.info}> <Message status={StatusEnum.info}>

View file

@ -2,7 +2,7 @@ import { Meta } from '@storybook/blocks';
<Meta title="Auth/Redux" /> <Meta title="Auth/Redux" />
# Redux slice # Redux
## Actions ## Actions
@ -10,13 +10,7 @@ import { Meta } from '@storybook/blocks';
`import { setAuthAttribute } from @procyon/auth/slice` `import { setAuthAttribute } from @procyon/auth/slice`
Update all avaible stored data in `auth` reducer. Update all available stored data in `auth` reducer.
### `setAuthTokens`
`import { setAuthTokens } from @procyon/auth/slice`
Set new tokens from IdP. Action will automaticaly calculate datetime, when tokens will be revoked.
### `updateAuthAttribute` ### `updateAuthAttribute`
@ -30,6 +24,12 @@ Update specific value in `auth` reducer.
Remove all data from reducer = revoke session. Remove all data from reducer = revoke session.
### `setAuthTokens`
`import { setAuthTokens } from @procyon/auth/slice`
Set new tokens from IdP. Action will automatically calculate datetime, when tokens will be revoked.
## Selectors ## Selectors
### getAuthAttribute ### getAuthAttribute
@ -37,3 +37,9 @@ Remove all data from reducer = revoke session.
`import { getAuthAttribute } from @procyon/auth/slice` `import { getAuthAttribute } from @procyon/auth/slice`
Return stored value for specific key under `auth` reducer. Return stored value for specific key under `auth` reducer.
## Hooks
### useAuthAttributes
Insted of `getAuthAttribute` you can use `useAuthAttributes` hook for obtain all stored attributes.

View file

@ -1,7 +1,7 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { useLocation } from 'wouter'; import { useLocation } from 'wouter';
import { useAuthCode } from '@procyon/auth/hook/useAuthCode'; import { useFetchTokens } from '@procyon/auth/hook/useFetchTokens';
import { useLogin } from '@procyon/auth/hook/useLogin'; import { useLogin } from '@procyon/auth/hook/useLogin';
const authConfig = { const authConfig = {
@ -17,13 +17,13 @@ export const Component: FC = () => {
const [location] = useLocation(); const [location] = useLocation();
const doLogin = useLogin(authConfig); const doLogin = useLogin(authConfig);
const changeCodeForTokens = useAuthCode(authConfig); const obtainTokens = useFetchTokens(authConfig);
if (location === '/login/callback') { if (location === '/login/callback') {
const params = new URLSearchParams(window.location.href); const params = new URLSearchParams(window.location.href);
const code = params.get('code') ?? null; const code = params.get('code') ?? null;
if (code) { if (code) {
changeCodeForTokens(code); obtainTokens(code);
} }
return null; return null;
} }