react-router-dom#Prompt TypeScript Examples
The following examples show how to use
react-router-dom#Prompt.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: index.tsx From uno-game with MIT License | 6 votes |
CloseGamePrompt: React.FC = () => {
const socket = useSocket()
const socketStore = useSocketStore()
const { gameId } = useParams<{ gameId: string }>()
const handleGoOutRoom = (newPathname: string): boolean => {
const isGoingOutGame = !newPathname.includes(gameId)
const isGoingOutTable = !newPathname.includes("table")
if (isGoingOutGame) {
socket.forceSelfDisconnect(gameId)
socketStore.setGameData({} as Game)
}
if (isGoingOutTable) {
GameEndedModal.close()
}
return true
}
return (
<Prompt message={(props) => handleGoOutRoom(props.pathname)} />
)
}
Example #2
Source File: ExperimentForm.tsx From abacus with GNU General Public License v2.0 | 4 votes |
ExperimentForm = ({
indexedMetrics,
indexedSegments,
initialExperiment,
onSubmit,
completionBag,
formSubmissionError,
}: {
indexedMetrics: Record<number, Metric>
indexedSegments: Record<number, Segment>
initialExperiment: ExperimentFormData
completionBag: ExperimentFormCompletionBag
onSubmit: (formData: unknown) => Promise<void>
formSubmissionError?: Error
}): JSX.Element => {
const classes = useStyles()
const rootRef = useRef<HTMLDivElement>(null)
const [currentStageId, setActiveStageId] = useState<StageId>(StageId.Beginning)
const currentStageIndex = stages.findIndex((stage) => stage.id === currentStageId)
const [completeStages, setCompleteStages] = useState<StageId[]>([])
const [errorStages, setErrorStages] = useState<StageId[]>([])
useEffect(() => {
rootRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'start' })
}, [currentStageId])
// Preventing accidental non-react-router navigate-aways:
const preventSubmissionRef = useRef<boolean>(false)
useEffect(() => {
// istanbul ignore next; trivial
// Sure we can test that these lines run but what is really important is how they
// behave in browsers, which IMO is too complicated to write tests for in this case.
const eventListener = (event: BeforeUnloadEvent) => {
if (preventSubmissionRef.current) {
event.preventDefault()
// Chrome requires returnValue to be set
event.returnValue = ''
}
}
window.addEventListener('beforeunload', eventListener)
return () => {
window.removeEventListener('beforeunload', eventListener)
}
}, [])
return (
<Formik
initialValues={{ experiment: initialExperiment }}
onSubmit={onSubmit}
validationSchema={yup.object({ experiment: experimentFullNewSchema })}
>
{(formikProps) => {
const getStageErrors = async (stage: Stage) => {
return _.pick(await formikProps.validateForm(), stage.validatableFields)
}
const isStageValid = async (stage: Stage): Promise<boolean> => {
const errors = await formikProps.validateForm()
return !stage.validatableFields.some((field) => _.get(errors, field))
}
const updateStageState = async (stage: Stage) => {
if (stage.id === StageId.Submit) {
return
}
if (await isStageValid(stage)) {
setErrorStages((prevValue) => _.difference(prevValue, [stage.id]))
setCompleteStages((prevValue) => _.union(prevValue, [stage.id]))
} else {
setErrorStages((prevValue) => _.union(prevValue, [stage.id]))
setCompleteStages((prevValue) => _.difference(prevValue, [stage.id]))
}
}
const changeStage = (stageId: StageId) => {
setActiveStageId(stageId)
void updateStageState(stages[currentStageIndex])
if (errorStages.includes(stageId)) {
void getStageErrors(stages[stageId]).then((stageErrors) =>
formikProps.setTouched(setNestedObjectValues(stageErrors, true)),
)
}
if (stageId === StageId.Submit) {
stages.map(updateStageState)
}
}
const prevStage = () => {
const prevStage = stages[currentStageIndex - 1]
prevStage && changeStage(prevStage.id)
}
const nextStage = () => {
const nextStage = stages[currentStageIndex + 1]
nextStage && changeStage(nextStage.id)
}
preventSubmissionRef.current = formikProps.dirty && !formikProps.isSubmitting
return (
<div className={classes.root}>
{/* This is required for React Router navigate-away prevention */}
<Prompt
when={preventSubmissionRef.current}
message='You have unsaved data, are you sure you want to leave?'
/>
<Paper className={classes.navigation}>
<Stepper nonLinear activeStep={currentStageId} orientation='horizontal'>
{stages.map((stage) => (
<Step key={stage.id} completed={stage.id !== currentStageId && completeStages.includes(stage.id)}>
<StepButton onClick={() => changeStage(stage.id)}>
<StepLabel error={stage.id !== currentStageId && errorStages.includes(stage.id)}>
{stage.title}
</StepLabel>
</StepButton>
</Step>
))}
</Stepper>
</Paper>
<div ref={rootRef}>
{/* Explanation: This should be fine as we aren't hiding behaviour that can't be accessed otherwise. */}
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */}
<form className={classes.form} onSubmit={formikProps.handleSubmit} noValidate>
{/* Prevent implicit submission of the form on enter. */}
{/* See https://stackoverflow.com/a/51507806 */}
<button type='submit' disabled style={{ display: 'none' }} aria-hidden='true'></button>
{currentStageId === StageId.Beginning && (
<div className={classes.formPart}>
<Paper className={classes.paper}>
<Beginning />
</Paper>
<div className={classes.formPartActions}>
<Button onClick={nextStage} variant='contained' color='primary'>
Begin
</Button>
</div>
</div>
)}
{currentStageId === StageId.BasicInfo && (
<div className={classes.formPart}>
<Paper className={classes.paper}>
<BasicInfo completionBag={completionBag} />
</Paper>
<div className={classes.formPartActions}>
<Button onClick={prevStage}>Previous</Button>
<Button onClick={nextStage} variant='contained' color='primary'>
Next
</Button>
</div>
</div>
)}
{currentStageId === StageId.Audience && (
<div className={classes.formPart}>
<Paper className={classes.paper}>
<Audience {...{ formikProps, indexedSegments, completionBag }} />
</Paper>
<div className={classes.formPartActions}>
<Button onClick={prevStage}>Previous</Button>
<Button onClick={nextStage} variant='contained' color='primary'>
Next
</Button>
</div>
</div>
)}
{currentStageId === StageId.Metrics && (
<div className={classes.formPart}>
<Paper className={classes.paper}>
<Metrics {...{ indexedMetrics, completionBag, formikProps }} />
</Paper>
<div className={classes.formPartActions}>
<Button onClick={prevStage}>Previous</Button>
<Button onClick={nextStage} variant='contained' color='primary'>
Next
</Button>
</div>
</div>
)}
{currentStageId === StageId.Submit && (
<div className={classes.formPart}>
<Paper className={classes.paper}>
<Typography variant='h4' gutterBottom>
Confirm and Submit Your Experiment
</Typography>
<Typography variant='body2' gutterBottom>
Now is a good time to{' '}
<Link href='https://github.com/Automattic/experimentation-platform/wiki' target='_blank'>
check our wiki's experiment creation checklist
</Link>{' '}
and confirm everything is in place.
</Typography>
<Typography variant='body2' gutterBottom>
Once you submit your experiment it will be set to staging, where it can be edited up until you
set it to running.
</Typography>
<Typography variant='body2' gutterBottom>
<strong> When you are ready, click the Submit button below.</strong>
</Typography>
</Paper>
<GeneralErrorAlert error={formSubmissionError} />
<div className={classes.formPartActions}>
<Button onClick={prevStage}>Previous</Button>
<LoadingButtonContainer isLoading={formikProps.isSubmitting}>
<Button
type='submit'
variant='contained'
color='secondary'
disabled={formikProps.isSubmitting || errorStages.length > 0}
>
Submit
</Button>
</LoadingButtonContainer>
</div>
</div>
)}
</form>
</div>
</div>
)
}}
</Formik>
)
}
Example #3
Source File: index.tsx From react-app-architecture with Apache License 2.0 | 4 votes |
export default function WritingPad(): ReactElement {
const classes = useStyles();
const dispatch = useDispatch();
const [preventBack, setPreventBack] = useState(false);
const [showPreview, setShowPreview] = useState(false);
const { hydrationBlog, data, isFetchingBlog, isSavingBlog, message } = useStateSelector(
({ writingPadState }) => writingPadState,
);
const [localState, setLocalState] = useState<LocalState>({
isForSubmission: false,
isAllDataSentToServer: false,
isBlogDetailsFormToShow: false,
title: '',
description: '',
imgUrl: '',
blogUrl: '',
tags: [],
isWriting: false,
isTitleError: false,
isDescriptionError: false,
isImgUrlError: false,
isBlogUrlError: false,
isTagsError: false,
});
useEffect(() => {
if (hydrationBlog?._id && !isFetchingBlog) dispatch(fetchBlog(hydrationBlog._id));
if (hydrationBlog)
setLocalState({
...localState,
title: hydrationBlog.title,
description: hydrationBlog.description,
imgUrl: hydrationBlog.imgUrl,
blogUrl: hydrationBlog.blogUrl,
tags: hydrationBlog.tags,
});
return () => {
dispatch(clearPad.action());
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleDoneClick = () => {
setLocalState({
...localState,
isBlogDetailsFormToShow: true,
isForSubmission: true,
});
};
const handleSaveClick = () => {
if (!data?._id) {
setLocalState({ ...localState, isBlogDetailsFormToShow: true });
} else {
setLocalState({ ...localState, isAllDataSentToServer: true });
data?._id &&
dispatch(
saveBlog(data._id, {
text: data.draftText,
title: localState.title,
description: localState.description,
tags: localState.tags,
imgUrl: localState.imgUrl,
}),
);
}
setPreventBack(false);
};
const handleCreateClick = () => {
data &&
dispatch(
createBlog({
text: data.draftText,
title: localState.title,
blogUrl: localState.blogUrl,
description: localState.description,
tags: localState.tags,
imgUrl: localState.imgUrl,
}),
);
};
const handleSubmitClick = () => {
setLocalState({ ...localState, isBlogDetailsFormToShow: false });
data?._id && dispatch(submitBlog(data._id));
dispatch(clearEditorPage.action());
};
const handleWithdrawClick = () => {
setLocalState({ ...localState, isBlogDetailsFormToShow: false });
data?._id && dispatch(withdrawBlog(data._id));
dispatch(clearEditorPage.action());
};
const renderMenu = () => {
if (!data) return null;
return (
<SpeedDial
direction="down"
ariaLabel="Blog Editor Menu"
className={classes.speedDial}
hidden={true}
icon={<ArrowDropDownIcon />}
open={true}
>
<SpeedDialAction
FabProps={{
disabled: !(data && data._id && data.isDraft),
}}
key="Done"
icon={<DoneAllIcon />}
tooltipTitle="Done"
onClick={handleDoneClick}
/>
<SpeedDialAction
key="Unsubmit"
FabProps={{
disabled: !(data && data._id && data.isSubmitted),
}}
icon={<CloseIcon />}
tooltipTitle="Remove Submission"
onClick={handleWithdrawClick}
/>
<SpeedDialAction
key="Submit"
FabProps={{
disabled: !(data && data._id && !data.isSubmitted),
}}
icon={<SendIcon />}
tooltipTitle="Submit Blog"
onClick={handleSubmitClick}
/>
<SpeedDialAction
FabProps={{
disabled: !(data?.draftText?.trim().length > 0),
}}
key="Blog Preview"
icon={<VisibilityIcon />}
tooltipTitle="Blog Preview"
onClick={() => setShowPreview(true)}
/>
<SpeedDialAction
FabProps={{
disabled: !(data?.draftText?.trim().length > 0),
}}
key="Save Blog"
icon={<SaveIcon />}
tooltipTitle="Save Blog"
onClick={handleSaveClick}
/>
</SpeedDial>
);
};
return (
<div className={classes.root}>
<Prompt
when={preventBack}
message={() => 'Are you sure you want to go without saving your work.'}
/>
{(isFetchingBlog || isSavingBlog) && <LinearProgress className={classes.progress} />}
<Grid className={classes.content} container justify="center">
<Grid item xs={12} sm={12} md={7}>
<TextareaAutosize
className={classes.pad}
aria-label="blog writing pad"
rowsMin={15}
value={data?.draftText}
onChange={(e) => {
dispatch(editBlog.action({ draftText: e.target.value }));
if (!preventBack) setPreventBack(true);
}}
placeholder="Write something awesome today.."
/>
</Grid>
</Grid>
{data && <Preview blog={data} open={showPreview} onClose={() => setShowPreview(false)} />}
<BlogDetailsForm
blog={data}
localState={localState}
setLocalState={setLocalState}
onSubmit={handleSubmitClick}
onCreate={handleCreateClick}
onSave={handleSaveClick}
/>
{renderMenu()}
{message && (
<Snackbar
message={message.text}
variant={message.type}
onClose={() => dispatch(removeMessage.action())}
/>
)}
</div>
);
}
Example #4
Source File: index.tsx From dnde with GNU General Public License v3.0 | 4 votes |
EditPage = () => {
const ref = useRef<any>(null);
const { templateId }: { templateId: string | undefined } = useParams();
const [trigger, { data, isError, isLoading, isSuccess }] = useLazyGetTemplateQuery();
useEffect(() => {
if (templateId === 'new' || typeof templateId === 'undefined') {
ref.current && ref.current.loadJson(null);
} else {
if (templateId) {
message.loading({ content: 'Fetching Template...', key: LOADING_KEY, duration: 0 });
trigger({ id: templateId });
}
}
}, []);
useEffect(() => {
if (isSuccess && data) {
try {
ref.current && ref.current.loadJson(data.response.data);
} catch (e) {
message.error('Unable to load template', 3);
}
} else if (isSuccess && !data) {
message.error('Template is empty', 2);
}
if (isSuccess) {
message.destroy(LOADING_KEY);
}
if (isError) {
message.info('Network error, template not fetched.', 2);
}
}, [isError, isLoading, isSuccess, data]);
const copyJsonInClipBoard = (e: any) => {
if (ref.current) {
e.preventDefault();
const json = ref.current.getJson();
logger.log('json', json);
navigator.clipboard.writeText(json);
success('Copied to Clipboard & logged in devtools ');
}
};
const copyHTMLAsClipBoard = (e: any) => {
if (ref.current) {
const html = ref.current.getHtml();
navigator.clipboard.writeText(html);
logger.log('html', html);
success('Copied to clipboard & logged in devtools ');
e.preventDefault();
}
};
const copyPreviewImage = async (e: any) => {
if (ref.current) {
e.preventDefault();
const html = ref.current.html;
navigator.clipboard.writeText(await generatePreview(html));
success('Preview Image Copied to clipboard');
}
};
return (
<div style={{ flex: '1', display: 'flex', width: '100%', height: '100%' }}>
<Row style={{ height: '100%', width: '100%' }} justify="center">
<Prompt
when={UNDOREDO.undo.length > 1 || UNDOREDO.redo.length > 1}
message={() => 'Are you sure you want to leave, your changes will be lost'}
/>
<Col lg={24} xl={0}>
<div style={{ textAlign: 'center', padding: '40px', paddingTop: '10%' }}>
<h3>Sorry, You need a device with a larger screen to perform editing, atleast '{'>'}=1200px'</h3>
</div>
</Col>
<Col xs={0} xl={24}>
<Layout style={{ height: '100%' }}>
<PageHeader
ghost={false}
onBack={() => window.history.back()}
title="dnde"
subTitle=""
style={{ borderBottom: '1px solid #e8e8e8' }}
extra={[
<>
<SendTestMail editorRef={ref} key="4" />
{/* <Button key="5" onClick={copyPreviewImage}>
Copy Preview Image
</Button> */}
<Button key="2" onClick={copyHTMLAsClipBoard}>
Copy as html
</Button>
<Button key="1" onClick={copyJsonInClipBoard}>
Copy as json
</Button>
</>,
]}
></PageHeader>
<Content>
<Editor ref={ref} />
</Content>
</Layout>
</Col>
</Row>
</div>
);
}
Example #5
Source File: index.tsx From easy-email with MIT License | 4 votes |
export function WarnAboutUnsavedChanges(props: WarnAboutUnsavedChangesProps) {
const { pageUnload = true } = props;
const formState = useFormState<any>();
const callbackRef = useRef<null | ((isOk: boolean) => any)>(null);
const [visible, setVisible] = useState(false);
const dirty = getIsFormTouched(formState.touched as any) || props.dirty;
const openConfirmModal = useCallback(() => {
setVisible(true);
}, []);
useEffect(() => {
ConfirmBeforeLeavePage.register((callback) => {
if (dirty) {
callbackRef.current = callback;
props.onBeforeConfirm?.();
openConfirmModal();
}
});
return () => {
ConfirmBeforeLeavePage.unregister();
};
}, [openConfirmModal, dirty, props]);
useEffect(() => {
if (pageUnload) {
const onCheckUnsaved = (event: Event) => {
if (dirty) {
props.onBeforeConfirm?.();
event.preventDefault();
(event.returnValue as any) =
'Changes that you made may not be saved.';
}
};
window.addEventListener('beforeunload', onCheckUnsaved);
return () => {
window.removeEventListener('beforeunload', onCheckUnsaved);
};
}
}, [dirty, pageUnload, props]);
const onCancel = useCallback(() => {
callbackRef.current?.(false);
setVisible(false);
}, []);
const onOk = useCallback(() => {
props.onBeforeConfirm?.();
callbackRef.current?.(true);
}, []);
return (
<>
<Modal
title='Discard changes?'
visible={visible}
onCancel={onCancel}
onOk={onOk}
okText='Discard'
cancelText='Cancel'
style={{ zIndex: 10000 }}
>
<p>Are you sure you want to discard all unsaved changes?</p>
</Modal>
{dirty && <Prompt when message='' />}
</>
);
}
Example #6
Source File: index.tsx From hub with Apache License 2.0 | 4 votes |
AuthorizationSection = (props: Props) => {
const { ctx, dispatch } = useContext(AppCtx);
const siteName = getMetaTag('siteName');
const updateActionBtn = useRef<RefActionBtn>(null);
const [apiError, setApiError] = useState<string | JSX.Element | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isSaving, setIsSaving] = useState<boolean>(false);
const [isTesting, setIsTesting] = useState<boolean>(false);
const [savedOrgPolicy, setSavedOrgPolicy] = useState<OrganizationPolicy | undefined>(undefined);
const [orgPolicy, setOrgPolicy] = useState<OrganizationPolicy | undefined | null>(undefined);
const [invalidPolicy, setInvalidPolicy] = useState<boolean>(false);
const [invalidPolicyDataJSON, setInvalidPolicyDataJSON] = useState<boolean>(false);
const [selectedOrg, setSelectedOrg] = useState<string | undefined>(undefined);
const [members, setMembers] = useState<string[] | undefined>(undefined);
const [notGetPolicyAllowed, setNotGetPolicyAllowed] = useState<boolean>(false);
const [updatePolicyAllowed, setUpdatePolicyAllowed] = useState<boolean>(false);
const [confirmationModal, setConfirmationModal] = useState<ConfirmationModal>({ open: false });
const getPredefinedPolicy = (name?: string): AuthorizationPolicy | undefined => {
let policy = PREDEFINED_POLICIES.find((item: AuthorizationPolicy) => item.name === name);
if (!isUndefined(policy) && !isUndefined(members)) {
policy = {
...policy,
data: {
roles: {
...policy.data.roles,
owner: {
users: members,
},
},
},
};
}
return policy;
};
const onPayloadChange = (e: ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
let updatedOrgPolicy: OrganizationPolicy | undefined = undefined;
if (value === 'predefined') {
if (savedOrgPolicy && savedOrgPolicy.predefinedPolicy) {
updatedOrgPolicy = {
...savedOrgPolicy,
authorizationEnabled: true,
};
} else {
const defaultPolicy = getPredefinedPolicy(DEFAULT_POLICY_NAME);
if (defaultPolicy) {
updatedOrgPolicy = {
...orgPolicy!,
customPolicy: null,
predefinedPolicy: defaultPolicy.name,
policyData: stringifyPolicyData(defaultPolicy.data),
};
}
}
checkPolicyChanges(
() => setOrgPolicy(updatedOrgPolicy!),
PolicyChangeAction.OnSwitchFromCustomToPredefinedPolicy
);
} else {
let updatedOrgPolicy: OrganizationPolicy | undefined = undefined;
if (savedOrgPolicy && savedOrgPolicy.customPolicy) {
updatedOrgPolicy = {
...savedOrgPolicy,
authorizationEnabled: true,
};
} else {
updatedOrgPolicy = {
...orgPolicy!,
customPolicy: null,
predefinedPolicy: null,
policyData: null,
};
}
checkPolicyChanges(
() => setOrgPolicy(updatedOrgPolicy!),
PolicyChangeAction.OnSwitchFromPredefinedToCustomPolicy
);
}
};
const checkIfUnsavedChanges = (): boolean => {
const lostData = checkUnsavedPolicyChanges(savedOrgPolicy!, orgPolicy!);
return lostData.lostData;
};
async function triggerTestInRegoPlayground() {
try {
setIsTesting(true);
let policy: string = '';
if (orgPolicy!.predefinedPolicy) {
const predefined = getPredefinedPolicy(orgPolicy!.predefinedPolicy);
if (predefined) {
policy = predefined.policy;
}
} else {
policy = orgPolicy!.customPolicy || '';
}
const data = prepareRegoPolicyForPlayground(policy, JSON.parse(orgPolicy!.policyData!), ctx.user!.alias);
const share: RegoPlaygroundResult = await API.triggerTestInRegoPlayground(data);
const popup = window.open(share.result, '_blank');
if (isNull(popup)) {
alertDispatcher.postAlert({
type: 'warning',
message:
'You have Pop-up windows blocked for this site. Please allow them so that we can open the OPA Playground for you.',
});
}
setIsTesting(false);
} catch (err: any) {
setIsTesting(false);
alertDispatcher.postAlert({
type: 'danger',
message: 'An error occurred opening the Playground, please try again later.',
});
}
}
async function getAuthorizationPolicy() {
try {
setIsLoading(true);
const policy = await API.getAuthorizationPolicy(selectedOrg!);
const formattedPolicy = {
authorizationEnabled: policy.authorizationEnabled,
predefinedPolicy: policy.predefinedPolicy || null,
customPolicy: policy.customPolicy || null,
policyData: policy.policyData ? stringifyPolicyData(policy.policyData) : null,
};
setSavedOrgPolicy(formattedPolicy);
setOrgPolicy(formattedPolicy);
setNotGetPolicyAllowed(false);
setUpdatePolicyAllowed(
authorizer.check({
organizationName: selectedOrg!,
action: AuthorizerAction.UpdateAuthorizationPolicy,
user: ctx.user!.alias,
})
);
setIsLoading(false);
} catch (err: any) {
setIsLoading(false);
if (err.kind === ErrorKind.Unauthorized) {
props.onAuthError();
} else if (err.kind === ErrorKind.Forbidden) {
setNotGetPolicyAllowed(true);
setOrgPolicy(null);
} else {
setNotGetPolicyAllowed(false);
setOrgPolicy(null);
alertDispatcher.postAlert({
type: 'danger',
message: 'An error occurred getting the policy from the organization, please try again later.',
});
}
}
}
async function updateAuthorizationPolicy() {
try {
setIsSaving(true);
await API.updateAuthorizationPolicy(selectedOrg!, orgPolicy!);
getAuthorizationPolicy();
// Update allowed actions and re-render button
authorizer.getAllowedActionsList(() => updateActionBtn.current!.reRender());
setIsSaving(false);
} catch (err: any) {
setIsSaving(false);
if (err.kind !== ErrorKind.Unauthorized) {
let error: string | JSX.Element = compoundErrorMessage(err, 'An error occurred updating the policy');
error = (
<>
{error}. For more information please see the{' '}
<ExternalLink
href="https://github.com/artifacthub/hub/blob/master/docs/authorization.md"
className="text-primary fw-bold"
label="Open documentation"
>
documentation
</ExternalLink>
.
</>
);
if (err.kind === ErrorKind.Forbidden) {
error = 'You do not have permissions to update the policy from the organization.';
setUpdatePolicyAllowed(false);
}
setApiError(error);
} else {
props.onAuthError();
}
}
}
async function fetchMembers() {
try {
const membersList: Member[] = await API.getAllOrganizationMembers(ctx.prefs.controlPanel.selectedOrg!);
setMembers(membersList.map((member: Member) => member.alias));
} catch (err: any) {
setMembers(undefined);
}
}
const onSaveAuthorizationPolicy = () => {
const policy = orgPolicy!.customPolicy || orgPolicy!.predefinedPolicy;
if (isNull(policy) || isUndefined(policy) || trim(policy) === '') {
setInvalidPolicy(true);
} else if (!isValidJSON(orgPolicy!.policyData || '')) {
setInvalidPolicyDataJSON(true);
} else {
checkPolicyChanges(updateAuthorizationPolicy, PolicyChangeAction.OnSavePolicy);
}
};
const onAuthorizationEnabledChange = () => {
let extraData = {};
const authorized = !orgPolicy!.authorizationEnabled;
const defaultPolicy = getPredefinedPolicy(DEFAULT_POLICY_NAME);
if (
authorized &&
(isNull(savedOrgPolicy!.customPolicy) || isUndefined(savedOrgPolicy!.customPolicy)) &&
(isNull(savedOrgPolicy!.predefinedPolicy) || isUndefined(savedOrgPolicy!.predefinedPolicy)) &&
!isUndefined(defaultPolicy)
) {
extraData = {
predefinedPolicy: defaultPolicy.name,
policyData: stringifyPolicyData(defaultPolicy.data),
};
}
const updatedOrgPolicy = {
...savedOrgPolicy!,
...extraData,
authorizationEnabled: authorized,
};
if (!authorized) {
checkPolicyChanges(() => setOrgPolicy(updatedOrgPolicy), PolicyChangeAction.OnDisableAuthorization);
} else {
setOrgPolicy(updatedOrgPolicy);
}
};
const onPredefinedPolicyChange = (e: ChangeEvent<HTMLSelectElement>) => {
e.preventDefault();
const activePredefinedPolicy = getPredefinedPolicy(e.target.value);
const updatedOrgPolicy = {
...orgPolicy!,
predefinedPolicy: e.target.value,
policyData: !isUndefined(activePredefinedPolicy) ? stringifyPolicyData(activePredefinedPolicy.data) : '',
};
checkPolicyChanges(() => setOrgPolicy(updatedOrgPolicy!), PolicyChangeAction.OnChangePredefinedPolicy);
};
const checkPolicyChanges = (onConfirmAction: () => void, action?: PolicyChangeAction) => {
const currentPredefinedPolicy =
orgPolicy && orgPolicy.predefinedPolicy ? getPredefinedPolicy(orgPolicy.predefinedPolicy) : undefined;
const lostData = checkUnsavedPolicyChanges(
savedOrgPolicy!,
orgPolicy!,
action,
currentPredefinedPolicy ? currentPredefinedPolicy.data : undefined
);
if (lostData.lostData) {
setConfirmationModal({
open: true,
message: lostData.message,
onConfirm: onConfirmAction,
});
} else {
onConfirmAction();
}
};
useEffect(() => {
if (selectedOrg) {
getAuthorizationPolicy();
fetchMembers();
}
}, [selectedOrg]); /* eslint-disable-line react-hooks/exhaustive-deps */
useEffect(() => {
if (ctx.prefs.controlPanel.selectedOrg) {
if (selectedOrg !== ctx.prefs.controlPanel.selectedOrg) {
if (!checkIfUnsavedChanges()) {
setSelectedOrg(ctx.prefs.controlPanel.selectedOrg);
} else {
const warningPrompt = window.confirm(
'You have some unsaved changes in your policy data. If you continue without saving, those changes will be lost.'
);
if (!warningPrompt) {
dispatch(updateOrg(selectedOrg!));
} else {
setSelectedOrg(ctx.prefs.controlPanel.selectedOrg);
}
}
}
}
}, [ctx.prefs.controlPanel.selectedOrg]); /* eslint-disable-line react-hooks/exhaustive-deps */
const onBeforeUnload = (e: BeforeUnloadEvent) => {
e.preventDefault();
e.returnValue =
'You have some unsaved changes in your policy data. If you continue without saving, those changes will be lost.';
};
useEffect(() => {
if (checkIfUnsavedChanges()) {
window.addEventListener('beforeunload', onBeforeUnload);
} else {
window.removeEventListener('beforeunload', onBeforeUnload);
}
return () => {
window.removeEventListener('beforeunload', onBeforeUnload);
};
}, [orgPolicy]); /* eslint-disable-line react-hooks/exhaustive-deps */
return (
<main role="main" className="p-0">
{(isUndefined(orgPolicy) || isLoading) && <Loading />}
<Prompt
when={!isNull(orgPolicy) && !isUndefined(orgPolicy) && !notGetPolicyAllowed && checkIfUnsavedChanges()}
message="You have some unsaved changes in your policy data. If you continue without saving, those changes will be lost."
/>
<div className={`h3 pb-2 border-bottom ${styles.title}`}>Authorization</div>
<div className="mt-4 mt-md-5" onClick={() => setApiError(null)}>
<p>
{siteName} allows you to setup fine-grained access control based on authorization policies. Authorization
polices are written in{' '}
<ExternalLink
href="https://www.openpolicyagent.org/docs/latest/#rego"
className="text-primary fw-bold"
label="Open rego documentation"
>
rego
</ExternalLink>{' '}
and they are evaluated using the{' '}
<ExternalLink
href="https://www.openpolicyagent.org"
className="text-primary fw-bold"
label="Open Open Policy Agent documentation"
>
Open Policy Agent
</ExternalLink>
. Depending on your requirements, you can use a predefined policy and only supply a data file, or you can
provide your custom policy for maximum flexibility. For more information please see the{' '}
<ExternalLink href="/docs/authorization" className="text-primary fw-bold" label="Open documentation">
documentation
</ExternalLink>
.
</p>
{(isNull(orgPolicy) || isUndefined(orgPolicy)) && notGetPolicyAllowed && (
<NoData>You are not allowed to manage this organization's authorization policy</NoData>
)}
{orgPolicy && (
<>
<div className="form-check form-switch mb-4">
<input
id="activeAuthorization"
type="checkbox"
className="form-check-input"
value="true"
role="switch"
onChange={onAuthorizationEnabledChange}
checked={orgPolicy.authorizationEnabled}
disabled={!updatePolicyAllowed}
/>
<label className="form-check-label" htmlFor="activeAuthorization">
Fine-grained access control
</label>
</div>
{orgPolicy.authorizationEnabled && (
<>
<label className={`form-label ${styles.label}`} htmlFor="payload">
<span className="fw-bold">Select authorization policy:</span>
</label>
<div className="d-flex flex-row mb-2">
{PAYLOAD_OPTION.map((item: Option) => {
const activeOption = !isNull(orgPolicy.predefinedPolicy) ? 'predefined' : 'custom';
return (
<div className="form-check me-4 mb-2" key={`payload_${item.name}`}>
<input
className="form-check-input"
type="radio"
id={item.name}
name="payload"
value={item.name}
checked={activeOption === item.name}
onChange={onPayloadChange}
disabled={!updatePolicyAllowed}
/>
<label className="form-check-label" htmlFor={item.name}>
{item.label}
</label>
</div>
);
})}
</div>
{orgPolicy.predefinedPolicy && (
<div className=" w-75 mb-4">
<select
className="form-select"
aria-label="org-select"
value={orgPolicy.predefinedPolicy || ''}
onChange={onPredefinedPolicyChange}
required={!isNull(orgPolicy.predefinedPolicy)}
disabled={!updatePolicyAllowed}
>
<option value="" disabled>
Select policy
</option>
{PREDEFINED_POLICIES.map((item: Option) => (
<option key={`policy_${item.name}`} value={item.name}>
{item.label}
</option>
))}
</select>
<div className={`invalid-feedback ${styles.fieldFeedback}`}>This field is required</div>
</div>
)}
<div className="d-flex flex-row align-self-stretch">
<div className="d-flex flex-column w-50 pe-2">
<div className="text-uppercase text-muted mb-2">Policy</div>
<div className="flex-grow-1">
<CodeEditor
mode="rego"
value={
orgPolicy.predefinedPolicy
? getPredefinedPolicy(orgPolicy.predefinedPolicy)!.policy
: orgPolicy.customPolicy
}
onChange={(value: string) => {
if (invalidPolicy) {
setInvalidPolicy(false);
}
setOrgPolicy({
...orgPolicy!,
customPolicy: value || null,
});
}}
disabled={orgPolicy.predefinedPolicy || !updatePolicyAllowed}
/>
{invalidPolicy && (
<small className="text-danger">
<span className="fw-bold">Error: </span> This field is required
</small>
)}
</div>
</div>
<div className="d-flex flex-column w-50 ps-2">
<div className="text-uppercase text-muted mb-2">Data</div>
<div className="flex-grow-1">
<CodeEditor
value={orgPolicy.policyData}
mode="javascript"
onChange={(value: string) => {
if (invalidPolicyDataJSON) {
setInvalidPolicyDataJSON(false);
}
setOrgPolicy({
...orgPolicy!,
policyData: value || null,
});
}}
disabled={!updatePolicyAllowed}
/>
{invalidPolicyDataJSON && (
<small className="text-danger">
<span className="fw-bold">Error: </span> Invalid JSON format
</small>
)}
</div>
</div>
</div>
</>
)}
<div className="d-flex flex-row mt-4">
{orgPolicy.authorizationEnabled && (
<button
type="button"
className="btn btn-sm btn-success"
onClick={triggerTestInRegoPlayground}
aria-label="Test in playground"
>
{isTesting ? (
<>
<span className="spinner-grow spinner-grow-sm" role="status" aria-hidden="true" />
<span className="ms-2">Preparing Playground...</span>
</>
) : (
<div className="d-flex flex-row align-items-center text-uppercase">
<RiTestTubeFill className="me-2" /> <div>Test in Playground</div>
</div>
)}
</button>
)}
<div className="ms-auto">
<ActionBtn
ref={updateActionBtn}
className="btn btn-sm btn-outline-secondary"
onClick={(e: ReactMouseEvent<HTMLButtonElement>) => {
e.preventDefault();
onSaveAuthorizationPolicy();
}}
action={AuthorizerAction.UpdateAuthorizationPolicy}
disabled={isSaving}
label="Update authorization policy"
>
<>
{isSaving ? (
<>
<span className="spinner-grow spinner-grow-sm" role="status" aria-hidden="true" />
<span className="ms-2">Saving</span>
</>
) : (
<div className="d-flex flex-row align-items-center text-uppercase">
<FaPencilAlt className="me-2" />
<div>Save</div>
</div>
)}
</>
</ActionBtn>
</div>
</div>
</>
)}
<Alert message={apiError} type="danger" onClose={() => setApiError(null)} />
</div>
{confirmationModal.open && (
<Modal
className={`d-inline-block ${styles.modal}`}
closeButton={
<>
<button
className="btn btn-sm btn-outline-secondary text-uppercase"
onClick={() => setConfirmationModal({ open: false })}
aria-label="Cancel"
>
Cancel
</button>
<button
className="btn btn-sm btn-outline-secondary text-uppercase ms-3"
onClick={(e) => {
e.preventDefault();
confirmationModal.onConfirm!();
setConfirmationModal({ open: false });
}}
aria-label="Confirm"
>
Ok
</button>
</>
}
header={<div className={`h3 m-2 flex-grow-1 ${styles.title}`}>Confirm action</div>}
onClose={() => setConfirmationModal({ open: false })}
open
>
<div className="mt-3 mw-100 text-center">
<p>{confirmationModal.message!}</p>
</div>
</Modal>
)}
</main>
);
}
Example #7
Source File: index.tsx From erda-ui with GNU Affero General Public License v3.0 | 4 votes |
ApiDesign = () => {
const [
{
contentKey,
dataTypeFormData,
filterKey,
apiResourceList,
apiDataTypeList,
quotePathMap,
treeModalVisible,
apiModalVisible,
curTreeNodeData,
curApiName,
curDataType,
newTreeNode,
popVisible,
apiDetail,
},
updater,
update,
] = useUpdate({
contentKey: 'SUMMARY',
dataTypeFormData: {},
filterKey: '',
apiResourceList: [] as string[],
apiDataTypeList: [] as string[],
quotePathMap: {} as Obj,
treeModalVisible: false,
apiModalVisible: false,
curTreeNodeData: {},
curApiName: '',
curDataType: '',
newTreeNode: {} as API_SETTING.IFileTree,
popVisible: false,
apiDetail: {},
});
const { inode: inodeQuery, pinode: pinodeQuery } = routeInfoStore.useStore((s) => s.query);
React.useEffect(() => {
const [key] = contentKey.split('&DICE&');
if (key === 'RESOURCE') {
curApiName && updater.contentKey(`${key}&DICE&${curApiName}`);
} else {
curDataType && updater.contentKey(`${key}&DICE&${curDataType}`);
}
}, [curApiName, contentKey, updater, curDataType]);
const { isExternalRepo, repoConfig } = appStore.useStore((s) => s.detail);
const [openApiDoc, apiWs, apiLockState, isDocChanged, wsQuery, formErrorNum, isApiReadOnly, lockUser, docValidData] =
apiDesignStore.useStore((s) => [
s.openApiDoc,
s.apiWs,
s.apiLockState,
s.isDocChanged,
s.wsQuery,
s.formErrorNum,
s.isApiReadOnly,
s.lockUser,
s.docValidData,
]);
const {
updateOpenApiDoc,
createTreeNode,
commitSaveApi,
getApiDetail,
publishApi,
updateFormErrorNum,
resetDocValidData,
} = apiDesignStore;
const [getApiDocDetailLoading, commitSaveApiLoading, getTreeListLoading] = useLoading(apiDesignStore, [
'getApiDetail',
'commitSaveApi',
'getTreeList',
]);
useMount(() => {
window.addEventListener('beforeunload', beforeunload);
});
useUnmount(() => {
updateOpenApiDoc({});
apiWs && apiWs.close();
window.removeEventListener('beforeunload', beforeunload);
});
const changeRef = React.useRef(null as any);
React.useEffect(() => {
changeRef.current = isDocChanged;
}, [isDocChanged]);
const beforeunload = React.useCallback((e) => {
const msg = `${i18n.t('dop:not saved yet, confirm to leave')}?`;
if (changeRef.current) {
// eslint-disable-next-line no-param-reassign
(e || window.event).returnValue = msg;
}
return msg;
}, []);
const apiResourceMap = React.useMemo(() => {
const tempMap = openApiDoc?.paths || {};
const fullKeys = keys(tempMap);
let tempList = [];
if (filterKey) {
tempList = filter(keys(tempMap), (name) => name.indexOf(filterKey) > -1);
} else {
tempList = fullKeys;
}
updater.apiResourceList(tempList);
return tempMap;
}, [filterKey, openApiDoc, updater]);
const apiDataTypeMap = React.useMemo(() => {
const tempMap = openApiDoc?.components?.schemas || {};
const fullKeys = keys(tempMap);
let tempList = [];
if (filterKey) {
tempList = filter(fullKeys, (name) => name.indexOf(filterKey) > -1);
} else {
tempList = fullKeys;
}
updater.apiDataTypeList(tempList);
return tempMap;
}, [filterKey, openApiDoc, updater]);
const onCreateDoc = (values: { name: string; pinode: string }) => {
createTreeNode(values).then((res) => {
updater.newTreeNode(res);
});
updater.treeModalVisible(false);
};
const onContentChange = React.useCallback(
(contentName: string) => {
const nextHandle = () => {
updateFormErrorNum(0);
const [, name] = contentName.split('&DICE&');
updater.contentKey(contentName);
if (contentName.startsWith('RESOURCE') && name) {
updater.curApiName(name);
const tempApiDetail = get(openApiDoc, ['paths', name]) || {};
updater.apiDetail(tempApiDetail);
}
if (contentName.startsWith('DATATYPE')) {
const _fromData = apiDataTypeMap[name] || { type: 'string', example: 'Example', 'x-dice-name': name };
updater.dataTypeFormData({ ..._fromData, name });
updater.curDataType(name);
}
};
if (formErrorNum > 0) {
confirm({
title: i18n.t('dop:Are you sure to leave, with the error message not saved?'),
onOk() {
nextHandle();
},
});
} else {
nextHandle();
}
},
[apiDataTypeMap, formErrorNum, openApiDoc, updateFormErrorNum, updater],
);
const dataTypeNameMap = React.useMemo(() => {
return keys(get(openApiDoc, ['components', 'schemas']));
}, [openApiDoc]);
const apiNameMap = React.useMemo(() => {
return keys(openApiDoc.paths || {});
}, [openApiDoc]);
const onAddHandle = (addKey: IListKey) => {
let newData = {};
let newName = `/api/new${apiResourceList.length}`;
while (apiNameMap.includes(newName)) {
newName += '1';
}
let dataPath = ['paths', newName];
if (addKey === 'DATATYPE') {
newName = `NewDataType${apiDataTypeList.length}`;
newData = { type: 'string', example: 'Example', 'x-dice-name': newName };
dataPath = ['components', 'schemas', newName];
}
const tempDocDetail = produce(openApiDoc, (draft) => set(draft, dataPath, newData));
updateOpenApiDoc(tempDocDetail);
onContentChange(`${addKey}&DICE&${newName}`);
};
const onDeleteHandle = (itemKey: string) => {
const [key, name] = itemKey.split('&DICE&');
if (key === 'DATATYPE') {
const newQuoteMap = getQuoteMap(openApiDoc);
if (newQuoteMap[name]?.length) {
message.warning(i18n.t('dop:this type is referenced and cannot be deleted'));
return;
}
} else if (key === 'RESOURCE') {
const paths = keys(openApiDoc.paths);
if (paths.length === 1) {
message.warning(i18n.t('dop:at least one API needs to be kept'));
return;
}
}
const dataPath = key === 'RESOURCE' ? ['paths', name] : ['components', 'schemas', name];
const tempDocDetail = produce(openApiDoc, (draft) => {
unset(draft, dataPath);
});
updateOpenApiDoc(tempDocDetail);
onContentChange('SUMMARY');
};
// 左侧列表头部渲染
const renderPanelHead = (titleKey: IListKey) => (
<div className="list-panel-head inline-flex justify-between items-center">
<span className="font-bold">{LIST_TITLE_MAP[titleKey]}</span>
{!apiLockState && (
<ErdaIcon
type="plus"
className="mr-0 cursor-pointer"
size="16px"
onClick={(e) => {
e.stopPropagation();
onAddHandle(titleKey);
}}
/>
)}
</div>
);
// 左侧列表渲染
const renderListItem = (listKey: IListKey, name: string) => {
const apiData = apiResourceMap[name] || {};
const key = `${listKey}&DICE&${name}`;
return (
<LazyRender key={name} minHeight={listKey === 'RESOURCE' ? '58px' : '37px'}>
<div
className={`list-title ${contentKey === key ? 'list-title-active' : ''}`}
onClick={() => onContentChange(key)}
>
<div className="flex justify-between items-center">
<Ellipsis title={name}>
<div className="list-title-name w-full nowrap mr-1">{name}</div>
</Ellipsis>
<Popconfirm
title={`${i18n.t('common:confirm to delete')}?`}
onConfirm={(e: any) => {
e.stopPropagation();
onDeleteHandle(key);
}}
onCancel={(e: any) => e.stopPropagation()}
>
{!apiLockState && (
<CustomIcon
type="shanchu"
className="list-title-btn cursor-pointer"
onClick={(e) => e?.stopPropagation()}
/>
)}
</Popconfirm>
</div>
{listKey === 'RESOURCE' && (
<div className="method-list">
{map(API_METHODS, (methodKey: API_SETTING.ApiMethod) => {
const methodIconClass = !isEmpty(apiData[methodKey]) ? `method-icon-${methodKey}` : '';
return (
<Tooltip title={methodKey} key={methodKey}>
<div className={`method-icon mr-2 ${methodIconClass}`} />
</Tooltip>
);
})}
</div>
)}
</div>
</LazyRender>
);
};
// 获取所有引用的pathMap
const getQuoteMap = React.useCallback(
(data: Obj) => {
const getQuotePath = (innerData: Obj, prefixPath: Array<number | string>, pathMap: Obj) => {
const refTypePath = get(innerData, [QUOTE_PREFIX, 0, '$ref']) || innerData[QUOTE_PREFIX_NO_EXTENDED];
if (refTypePath) {
const _type = refTypePath.split('/').slice(-1)[0];
// eslint-disable-next-line no-param-reassign
!pathMap[_type] && (pathMap[_type] = []);
if (!pathMap[_type].includes(prefixPath)) {
pathMap[_type].push(prefixPath);
}
}
if (innerData?.properties) {
forEach(keys(innerData.properties), (item) => {
getQuotePath(innerData.properties[item], [...prefixPath, 'properties', item], pathMap);
});
}
if (innerData?.items) {
getQuotePath(innerData.items, [...prefixPath, 'items'], pathMap);
}
};
const tempMap = {};
const pathMap = data.paths;
forEach(keys(pathMap), (path) => {
const pathData = pathMap[path];
forEach(keys(pathData), (method) => {
const methodData = pathData[method];
const _path = ['paths', path, method];
forEach(API_MEDIA_TYPE, (mediaType) => {
// responses
const responsePath = ['responses', '200', 'content', mediaType, 'schema'];
const responseData = get(methodData, responsePath) || {};
getQuotePath(responseData, [..._path, ...responsePath], tempMap);
// requestBody;
const requestBodyPath = ['requestBody', 'content', mediaType, 'schema'];
const requestBody = get(methodData, requestBodyPath) || {};
getQuotePath(requestBody, [..._path, ...requestBodyPath], tempMap);
});
// parameters
const parametersData = methodData.parameters || [];
forEach(parametersData, (pData, index) => {
getQuotePath(pData, [..._path, 'parameters', index], tempMap);
});
});
});
// datatype中的引用
const dataTypeData = data?.components?.schemas || {};
forEach(keys(dataTypeData), (dataTypeName) => {
getQuotePath(dataTypeData[dataTypeName], ['components', 'schemas', dataTypeName], tempMap);
});
updater.quotePathMap(tempMap);
return tempMap;
},
[updater],
);
const onQuotePathMapChange = React.useCallback(
(pathMap: Obj) => {
updater.quotePathMap(pathMap);
},
[updater],
);
const onApiNameChange = React.useCallback(
(name: string) => {
updater.curApiName(name);
},
[updater],
);
const renderContent = (key: string) => {
if (key.startsWith('RESOURCE')) {
return (
<ApiResource
onQuoteChange={onQuotePathMapChange}
onApiNameChange={onApiNameChange}
quotePathMap={quotePathMap}
apiName={curApiName}
apiDetail={apiDetail}
/>
);
} else if (key.startsWith('DATATYPE')) {
return (
<DataTypeConfig
quotePathMap={quotePathMap}
dataTypeNameMap={dataTypeNameMap}
formData={dataTypeFormData}
key={dataTypeFormData?.name}
dataType={curDataType}
onQuoteNameChange={onQuotePathMapChange}
onDataTypeNameChange={(name) => updater.curDataType(name)}
isEditMode={!apiLockState}
/>
);
} else {
return <ApiSummary />;
}
};
const isDocLocked = React.useMemo(() => {
return wsQuery?.sessionID && apiLockState;
}, [apiLockState, wsQuery]);
const LockTipVisible = React.useMemo(() => isApiReadOnly || isDocLocked, [isApiReadOnly, isDocLocked]);
const docLockTip = React.useMemo(() => {
if (isApiReadOnly) {
return i18n.t('dop:protect branch, not editable');
} else if (isDocLocked) {
return lockUser + API_LOCK_WARNING;
} else {
return '';
}
}, [isApiReadOnly, isDocLocked, lockUser]);
const errorData = React.useMemo(() => {
return {
branchName: curTreeNodeData.branchName,
docName: `${curTreeNodeData.apiDocName}.yaml`,
msg: docValidData.msg,
};
}, [curTreeNodeData, docValidData]);
const onEditDocHandle = () => {
if (!apiWs) {
initApiWs({ inode: inodeQuery, pinode: pinodeQuery });
} else if (isDocLocked) {
message.warning(lockUser + API_LOCK_WARNING);
}
};
const onPublishApi = React.useCallback(
(values: any) => {
publishApi(values).then(() => {
apiWs && apiWs.close();
getApiDetail(inodeQuery as string).then((data: any) => {
getQuoteMap(data.openApiDoc);
updater.curTreeNodeData({
...curTreeNodeData,
asset: data.asset,
});
});
});
},
[apiWs, curTreeNodeData, getApiDetail, getQuoteMap, inodeQuery, publishApi, updater],
);
const onSelectDoc = React.useCallback(
(nodeData, reset) => {
if (reset) {
updateOpenApiDoc({});
resetDocValidData();
}
onContentChange('Summary');
update({
contentKey: 'SUMMARY',
curTreeNodeData: nodeData,
newTreeNode: {} as API_SETTING.IFileTree,
filterKey: '',
});
},
[onContentChange, resetDocValidData, update, updateOpenApiDoc],
);
const onToggleTreeVisible = React.useCallback(
(val: boolean) => {
updater.popVisible(val);
},
[updater],
);
const onConfirmPublish = React.useCallback(() => {
if (isDocChanged) {
confirm({
title: i18n.t('dop:The current document has not been saved. Publish the saved document?'),
onOk() {
updater.apiModalVisible(true);
},
});
} else {
updater.apiModalVisible(true);
}
}, [isDocChanged, updater]);
const showErrorDocTip = React.useMemo(() => {
return !docValidData.valid && !isDocChanged && !isEmpty(openApiDoc);
}, [docValidData.valid, isDocChanged, openApiDoc]);
return isExternalRepo === undefined ? (
<EmptyHolder relative />
) : (
<div className="api-design">
<TopButtonGroup>
<Button type="primary" onClick={() => updater.treeModalVisible(true)}>
{i18n.t('dop:New Document')}
</Button>
</TopButtonGroup>
<div className="api-design-wrap">
<div className="search-wrap mb-4 flex items-center justify-start">
<ApiDocTree
treeNodeData={curTreeNodeData}
newTreeNode={newTreeNode}
getQuoteMap={getQuoteMap}
onSelectDoc={onSelectDoc}
popVisible={popVisible}
onVisibleChange={onToggleTreeVisible}
/>
{LockTipVisible && (
<span className="ml-4">
<CustomIcon type="lock" />
{docLockTip}
</span>
)}
{showErrorDocTip && <ErrorPopover {...errorData} />}
{inodeQuery && !isEmpty(curTreeNodeData) && (
<div className="flex items-center flex-wrap justify-end flex-1">
{!apiWs || isDocLocked ? (
<WithAuth pass={!isApiReadOnly && docValidData.valid}>
<Button type="ghost" onClick={onEditDocHandle}>
{i18n.t('Edit')}
</Button>
</WithAuth>
) : (
<Button type="ghost" disabled={formErrorNum > 0} onClick={() => commitSaveApi()}>
{i18n.t('Save')}
</Button>
)}
<WithAuth pass={inodeQuery && docValidData.valid}>
<Button type="primary" className="ml-2" onClick={onConfirmPublish}>
{i18n.t('publisher:Release')}
</Button>
</WithAuth>
</div>
)}
</div>
<Spin spinning={getApiDocDetailLoading || commitSaveApiLoading || getTreeListLoading}>
{isEmpty(openApiDoc) ? (
<ErrorEmptyHolder {...errorData} isLoading={getTreeListLoading} />
) : (
<div className="api-design-content">
<div className="api-design-content-list flex flex-col justify-start">
<Input
placeholder={i18n.t('Search by keyword')}
className="mx-2 my-3 api-filter-input"
prefix={<ErdaIcon type="search1" size="14" className="mr-0.5 mt-0.5" />}
onInput={(e: React.ChangeEvent<HTMLInputElement>) => updater.filterKey(e.target.value)}
/>
<div
className={`list-title py-3 border-bottom font-bold ${
contentKey === 'SUMMARY' ? 'list-title-active' : ''
}`}
onClick={() => onContentChange('SUMMARY')}
>
{i18n.t('dop:API overview')}
</div>
<div className="panel-list">
<Collapse
accordion
bordered={false}
defaultActiveKey={['RESOURCE']}
className="api-overview-collapse"
>
<Panel header={renderPanelHead('RESOURCE')} key="RESOURCE">
{!isEmpty(apiResourceList) ? (
map(apiResourceList, (name) => renderListItem('RESOURCE', name))
) : (
<EmptyHolder relative />
)}
</Panel>
<Panel header={renderPanelHead('DATATYPE')} key="DATATYPE">
{!isEmpty(apiDataTypeList) ? (
map(apiDataTypeList, (name) => renderListItem('DATATYPE', name))
) : (
<EmptyHolder relative />
)}
</Panel>
</Collapse>
</div>
</div>
<div className="api-design-content-detail px-4 py-3">{renderContent(contentKey)}</div>
</div>
)}
</Spin>
<ApiDocAddModal
visible={treeModalVisible}
onClose={() => updater.treeModalVisible(false)}
onSubmit={onCreateDoc}
/>
<ApiPublishModal
visible={apiModalVisible}
treeNodeData={curTreeNodeData as API_SETTING.ITreeNodeData}
onSubmit={onPublishApi}
onClose={() => updater.apiModalVisible(false)}
/>
</div>
<Prompt
when={isDocChanged}
message={(location: Location) => {
if (location.pathname.endsWith('apiDesign')) {
return false;
}
return `${i18n.t('dop:not saved yet, confirm to leave')}?`;
}}
/>
</div>
);
}
Example #8
Source File: perm-editor.tsx From erda-ui with GNU Affero General Public License v3.0 | 4 votes |
PermEditor = (props: IProps) => {
const [{ scope = 'org', mode }, { projectId }] = routeInfoStore.getState((s) => [s.query, s.params]);
const isMsp = props.scope === 'msp';
const permData = isMsp ? props.data : permDatas;
const originRoleMap = isMsp ? props.roleMap : roleMaps;
const defaultScope = isMsp ? props.scope : scope;
const [{ data, tabKey, searchKey, roleMap, reloadKey }, updater, update] = useUpdate({
data: permData,
tabKey: defaultScope,
searchKey: '',
roleMap: originRoleMap,
reloadKey: 1,
});
// 默认在项目下,即为编辑状态: project/:id/perm
// 在根路由下,即为查看状态:/perm
const isEdit = isMsp ? mode === 'edit' : !!projectId;
const onChangeRole = (_data: IRoleChange) => {
const { key, role, checked } = _data;
const newData = produce(data, (draft) => {
if (role) {
const curDataRole = get(draft, `${key}.role`);
set(draft, `${key}.role`, checked ? uniq([...curDataRole, role]) : filter(curDataRole, (r) => r !== role));
}
});
updater.data(newData);
};
const deleteData = (_dataKey: string) => {
const newData = produce(data, (draft) => {
set(draft, _dataKey, undefined);
});
updater.data(newData);
};
const editData = (val: Obj) => {
const { key, name, keyPath, preKey, subData } = val;
const newData = produce(data, (draft) => {
const curKeyPath = keyPath ? `${keyPath}.` : '';
const curData = get(draft, `${curKeyPath}${preKey}`);
if (subData) {
set(draft, `${curKeyPath}${preKey}`, { ...curData, ...subData });
} else {
set(draft, `${curKeyPath}${preKey}`, undefined);
set(draft, `${curKeyPath}${key}`, { ...curData, name });
}
});
updater.data(newData);
};
const reset = () => {
update({
data: permData,
tabKey: defaultScope,
searchKey: '',
roleMap: originRoleMap,
reloadKey: reloadKey + 1,
});
};
const addScope = (newScope: Obj) => {
const { key, name } = newScope;
update({
data: { ...data, [key]: { name, test: { pass: false, name: '测试权限', role: ['Manager'] } } },
roleMap: { ...originRoleMap, [key]: defaultRole },
});
};
const updateRole = (roleData: Obj<IRoleData>) => {
update({
roleMap: {
...roleMap,
[tabKey]: roleData,
},
reloadKey: reloadKey + 1,
});
};
return (
<div className="dice-perm-editor h-full ml-4 bg-white rounded px-4">
{isEdit ? (
<TopButtonGroup>
<AddScope onSubmit={addScope} currentData={data} />
<Button type="primary" ghost onClick={reset}>
{i18n.t('reset')}
</Button>
</TopButtonGroup>
) : null}
<Prompt when={isEdit} message={`${i18n.t('Are you sure to leave?')}?`} />
<Tabs
activeKey={tabKey}
tabBarExtraContent={
<div className="flex justify-between items-center mt-2">
<DebounceSearch size="small" value={searchKey} className="mr-2" onChange={updater.searchKey} />
{isEdit ? (
<>
<PermExport
activeScope={tabKey}
roleMap={roleMap}
data={data}
projectId={projectId}
onSubmit={updater.data}
isEdit
/>
<PermRoleEditor data={roleMap[tabKey]} updateRole={updateRole} />
</>
) : null}
</div>
}
renderTabBar={(p: any, DefaultTabBar) => <DefaultTabBar {...p} onKeyDown={(e: any) => e} />}
onChange={(curKey: string) => update({ searchKey: '', tabKey: curKey })}
>
{map(data, (item: IPermItem, key: string) => {
if (!item) return null;
const { name } = item;
return (
<TabPane tab={name} key={key}>
<PermTable
data={item}
scope={key}
key={reloadKey}
filterKey={searchKey}
isEdit={isEdit}
roleMap={getRoleMap(roleMap, isEdit)}
onChangeRole={onChangeRole}
deleteData={deleteData}
editData={editData}
originData={permData}
originRoleMap={originRoleMap}
currentData={data}
/>
</TabPane>
);
})}
</Tabs>
</div>
);
}
Example #9
Source File: Editor.tsx From legend-studio with Apache License 2.0 | 4 votes |
EditorInner = observer(() => {
const params = useParams<EditorPathParams | GroupEditorPathParams>();
const projectId = params.projectId;
const workspaceType = (params as { groupWorkspaceId: string | undefined })
.groupWorkspaceId
? WorkspaceType.GROUP
: WorkspaceType.USER;
const workspaceId =
workspaceType === WorkspaceType.GROUP
? (params as GroupEditorPathParams).groupWorkspaceId
: (params as EditorPathParams).workspaceId;
const editorStore = useEditorStore();
const applicationStore = useApplicationStore();
// Extensions
const extraEditorExtensionComponents = editorStore.pluginManager
.getStudioPlugins()
.flatMap(
(plugin) =>
plugin.getExtraEditorExtensionComponentRendererConfigurations?.() ?? [],
)
.filter(isNonNullable)
.map((config) => (
<Fragment key={config.key}>{config.renderer(editorStore)}</Fragment>
));
// Resize
const { ref, width, height } = useResizeDetector<HTMLDivElement>();
// These create snapping effect on panel resizing
const resizeSideBar = (handleProps: ResizablePanelHandlerProps): void =>
editorStore.sideBarDisplayState.setSize(
(handleProps.domElement as HTMLDivElement).getBoundingClientRect().width,
);
const resizeAuxPanel = (handleProps: ResizablePanelHandlerProps): void =>
editorStore.auxPanelDisplayState.setSize(
(handleProps.domElement as HTMLDivElement).getBoundingClientRect().height,
);
useEffect(() => {
if (ref.current) {
editorStore.auxPanelDisplayState.setMaxSize(ref.current.offsetHeight);
}
}, [editorStore, ref, height, width]);
// Hotkeys
const [hotkeyMapping, hotkeyHandlers] = buildReactHotkeysConfiguration(
editorStore.hotkeys,
);
// Cleanup the editor
useEffect(() => (): void => editorStore.cleanUp(), [editorStore]);
// Initialize the app
useEffect(() => {
flowResult(
editorStore.initialize(projectId, workspaceId, workspaceType),
).catch(applicationStore.alertUnhandledError);
}, [editorStore, applicationStore, projectId, workspaceId, workspaceType]);
// Browser Navigation Blocking (reload, close tab, go to another URL)
// NOTE: there is no way to customize the alert message for now since Chrome removed support for it
// See https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload#Browser_compatibility
// There is also no way to customize this modal style-wise so we would only be able to do so for route navigation blocking
// See https://medium.com/@jozollo/blocking-navigation-with-react-router-v4-a3f2e359d096
useEffect(() => {
const onUnload = (event: BeforeUnloadEvent): void => {
/**
* NOTE: one subtle trick here. Since we have to use `useEffect` to set the event listener for `beforeunload` event,
* we have to be be careful, if we extract `editorStore.ignoreNavigationBlock` out to a variable such as `ignoreNavigationBlock`
* and then make this `useEffect` be called in response to that, there is a chance that if we set `editorStore.ignoreNavigationBlock`
* to `true` and then go on to call systematic refresh (i.e. `window.location.reload()`) immediately, the event listener on window will
* become stale and still show the blocking popup.
*
* This is almost guaranteed to happen as `useEffect` occurs after rendering, and thus will defnitely be called after the immediate
* `window.location.reload()`. As such, the best way is instead of expecting `useEffect` to watch out for the change in `ignoreNavigationBlock`
* we will access the value of `ignoreNavigationBlock` in the body of the `onUnload` function to make it more dynamic. This ensures the
* event listener will never go stale
*/
const showAlert =
editorStore.isInConflictResolutionMode ||
editorStore.hasUnpushedChanges;
if (!editorStore.ignoreNavigationBlocking && showAlert) {
event.returnValue = '';
}
};
window.removeEventListener('beforeunload', onUnload);
window.addEventListener('beforeunload', onUnload);
return (): void => window.removeEventListener('beforeunload', onUnload);
}, [editorStore]);
// Route Navigation Blocking
// See https://medium.com/@michaelchan_13570/using-react-router-v4-prompt-with-custom-modal-component-ca839f5faf39
const [blockedLocation, setBlockedLocation] = useState<
Location | undefined
>();
const retryBlockedLocation = useCallback(
(allowedNavigation: boolean): void => {
if (allowedNavigation && blockedLocation) {
applicationStore.navigator.goTo(blockedLocation.pathname);
}
},
[blockedLocation, applicationStore],
);
// NOTE: we have to use `useStateWithCallback` here because we want to guarantee that we call `history.push(blockedLocation.pathname)`
// after confirmedAllowNavigation is flipped, otherwise we would end up in the `false` case of handleBlockedNavigation again!
// Another way to go about this is to use `setTimeout(() => history.push(...), 0)` but it can potentially be more error-prone
// See https://www.robinwieruch.de/react-usestate-callback
const [confirmedAllowNavigation, setConfirmedAllowNavigation] =
useStateWithCallback<boolean>(false, retryBlockedLocation);
const onNavigationChangeIndicator = Boolean(
editorStore.changeDetectionState.workspaceLocalLatestRevisionState.changes
.length,
);
const handleRouteNavigationBlocking = (nextLocation: Location): boolean => {
// NOTE: as long as we're in conflict resolution, we want this block to be present
const showAlert =
editorStore.isInConflictResolutionMode || editorStore.hasUnpushedChanges;
if (
!editorStore.ignoreNavigationBlocking &&
!confirmedAllowNavigation &&
showAlert
) {
editorStore.setActionAlertInfo({
message: editorStore.isInConflictResolutionMode
? 'You have not accepted the conflict resolution, the current resolution will be discarded. Leave anyway?'
: 'You have unpushed changes. Leave anyway?',
type: ActionAlertType.CAUTION,
onEnter: (): void => editorStore.setBlockGlobalHotkeys(true),
onClose: (): void => editorStore.setBlockGlobalHotkeys(false),
actions: [
{
label: 'Leave this page',
type: ActionAlertActionType.PROCEED_WITH_CAUTION,
handler: (): void => setConfirmedAllowNavigation(true),
},
{
label: 'Stay on this page',
type: ActionAlertActionType.PROCEED,
default: true,
handler: (): void => setBlockedLocation(undefined),
},
],
});
setBlockedLocation(nextLocation);
return false;
}
// Reset the confirm flag and the blocked location here
setBlockedLocation(undefined);
setConfirmedAllowNavigation(false);
return true;
};
const editable =
editorStore.graphManagerState.graphBuildState.hasCompleted &&
editorStore.isInitialized;
const isResolvingConflicts =
editorStore.isInConflictResolutionMode &&
!editorStore.conflictResolutionState.hasResolvedAllConflicts;
const promptComponent = (
<Prompt
when={onNavigationChangeIndicator}
message={handleRouteNavigationBlocking}
/>
);
useApplicationNavigationContext(
LEGEND_STUDIO_APPLICATION_NAVIGATION_CONTEXT.EDITOR,
);
return (
<div className="app__page">
<div className="editor">
{promptComponent}
<GlobalHotKeys
keyMap={hotkeyMapping}
handlers={hotkeyHandlers}
allowChanges={true}
>
<div className="editor__body">
<ActivityBar />
<Backdrop className="backdrop" open={editorStore.backdrop} />
<div ref={ref} className="editor__content-container">
<div className="editor__content">
<ResizablePanelGroup orientation="vertical">
<ResizablePanel
{...getControlledResizablePanelProps(
editorStore.sideBarDisplayState.size === 0,
{
onStopResize: resizeSideBar,
},
)}
size={editorStore.sideBarDisplayState.size}
direction={1}
>
<SideBar />
</ResizablePanel>
<ResizablePanelSplitter />
<ResizablePanel minSize={300}>
<ResizablePanelGroup orientation="horizontal">
<ResizablePanel
{...getControlledResizablePanelProps(
editorStore.auxPanelDisplayState.isMaximized,
)}
>
{(isResolvingConflicts || editable) &&
editorStore.isInFormMode && <EditPanel />}
{editable && editorStore.isInGrammarTextMode && (
<GrammarTextEditor />
)}
{!editable && <EditPanelSplashScreen />}
</ResizablePanel>
<ResizablePanelSplitter>
<ResizablePanelSplitterLine
color={
editorStore.auxPanelDisplayState.isMaximized
? 'transparent'
: 'var(--color-dark-grey-250)'
}
/>
</ResizablePanelSplitter>
<ResizablePanel
{...getControlledResizablePanelProps(
editorStore.auxPanelDisplayState.size === 0,
{
onStopResize: resizeAuxPanel,
},
)}
flex={0}
direction={-1}
size={editorStore.auxPanelDisplayState.size}
>
<AuxiliaryPanel />
</ResizablePanel>
</ResizablePanelGroup>
</ResizablePanel>
</ResizablePanelGroup>
</div>
</div>
</div>
{extraEditorExtensionComponents}
<StatusBar actionsDisabled={!editable} />
{editable && <ProjectSearchCommand />}
{editorStore.localChangesState.workspaceSyncState
.workspaceSyncConflictResolutionState.showModal && (
<WorkspaceSyncConflictResolver />
)}
</GlobalHotKeys>
</div>
</div>
);
})
Example #10
Source File: FlowEditor.tsx From glific-frontend with GNU Affero General Public License v3.0 | 4 votes |
FlowEditor = (props: FlowEditorProps) => {
const { match } = props;
const history = useHistory();
const { uuid } = match.params;
const [publishDialog, setPublishDialog] = useState(false);
const [simulatorId, setSimulatorId] = useState(0);
const [loading, setLoading] = useState(true);
const config = setConfig(uuid);
const [published, setPublished] = useState(false);
const [stayOnPublish, setStayOnPublish] = useState(false);
const [modalVisible, setModalVisible] = useState(false);
const [showResetFlowModal, setShowResetFlowModal] = useState(false);
const [lastLocation, setLastLocation] = useState<Location | null>(null);
const [confirmedNavigation, setConfirmedNavigation] = useState(false);
const [flowValidation, setFlowValidation] = useState<any>();
const [IsError, setIsError] = useState(false);
const [flowKeyword, setFlowKeyword] = useState('');
const [currentEditDialogBox, setCurrentEditDialogBox] = useState(false);
const [dialogMessage, setDialogMessage] = useState('');
const { drawerOpen } = useContext(SideDrawerContext);
let modal = null;
let dialog = null;
let flowTitle: any;
const [getOrganizationServices] = useLazyQuery(GET_ORGANIZATION_SERVICES, {
fetchPolicy: 'network-only',
onCompleted: (services) => {
const { dialogflow, googleCloudStorage, flowUuidDisplay } = services.organizationServices;
if (googleCloudStorage) {
config.attachmentsEnabled = true;
}
if (!dialogflow) {
config.excludeTypes.push('split_by_intent');
}
if (flowUuidDisplay) {
config.showNodeLabel = true;
}
showFlowEditor(document.getElementById('flow'), config);
setLoading(false);
},
});
const [getFreeFlow] = useLazyQuery(GET_FREE_FLOW, {
fetchPolicy: 'network-only',
onCompleted: ({ flowGet }) => {
if (flowGet.flow) {
getOrganizationServices();
} else if (flowGet.errors && flowGet.errors.length) {
setDialogMessage(flowGet.errors[0].message);
setCurrentEditDialogBox(true);
}
},
});
const [exportFlowMutation] = useLazyQuery(EXPORT_FLOW, {
fetchPolicy: 'network-only',
onCompleted: async ({ exportFlow }) => {
const { exportData } = exportFlow;
exportFlowMethod(exportData, flowTitle);
},
});
const [resetFlowCountMethod] = useMutation(RESET_FLOW_COUNT, {
onCompleted: ({ resetFlowCount }) => {
const { success } = resetFlowCount;
if (success) {
setNotification('Flow counts have been reset', 'success');
setShowResetFlowModal(false);
window.location.reload();
}
},
});
const [publishFlow] = useMutation(PUBLISH_FLOW, {
onCompleted: (data) => {
if (data.publishFlow.errors && data.publishFlow.errors.length > 0) {
setFlowValidation(data.publishFlow.errors);
setIsError(true);
} else if (data.publishFlow.success) {
setPublished(true);
}
},
});
const { data: flowName } = useQuery(GET_FLOW_DETAILS, {
fetchPolicy: 'network-only',
variables: {
filter: {
uuid,
},
opts: {},
},
});
let flowId: any;
// flowname can return an empty array if the uuid present is not correct
if (flowName && flowName.flows.length > 0) {
flowTitle = flowName.flows[0].name;
flowId = flowName.flows[0].id;
}
const closeModal = () => {
setModalVisible(false);
};
const handleBlockedNavigation = (nextLocation: any): boolean => {
if (!confirmedNavigation) {
setModalVisible(true);
setLastLocation(nextLocation);
return false;
}
return true;
};
const handleConfirmNavigationClick = () => {
setModalVisible(false);
setConfirmedNavigation(true);
};
useEffect(() => {
if (confirmedNavigation && lastLocation) {
history.push(lastLocation);
}
}, [confirmedNavigation, lastLocation, history]);
if (modalVisible) {
modal = (
<DialogBox
title="Unsaved changes!"
handleOk={handleConfirmNavigationClick}
handleCancel={closeModal}
colorOk="secondary"
buttonOk="Ignore & leave"
buttonCancel="Stay & recheck"
alignButtons="center"
contentAlign="center"
additionalTitleStyles={styles.DialogTitle}
>
<div className={styles.DialogContent}>
Your changes will not be saved if you navigate away. Please save as draft or publish.
</div>
</DialogBox>
);
}
const handleResetFlowCount = () => {
resetFlowCountMethod({ variables: { flowId } });
};
if (showResetFlowModal) {
modal = (
<DialogBox
title="Warning!"
handleOk={handleResetFlowCount}
handleCancel={() => setShowResetFlowModal(false)}
colorOk="secondary"
buttonOk="Accept & reset"
buttonCancel="DON'T RESET YET"
alignButtons="center"
contentAlign="center"
additionalTitleStyles={styles.DialogTitle}
>
<div className={styles.DialogContent}>
Please be careful, this cannot be undone. Once you reset the flow counts you will lose
tracking of how many times a node was triggered for users.
</div>
</DialogBox>
);
}
useEffect(() => {
if (flowName) {
document.title = flowTitle;
}
return () => {
document.title = APP_NAME;
};
}, [flowName]);
useEffect(() => {
if (flowId) {
const { fetch, xmlSend, xmlOpen } = setAuthHeaders();
const files = loadfiles(() => {
getFreeFlow({ variables: { id: flowId } });
});
// when switching tabs we need to check if the flow is still active for the user
window.onfocus = () => {
getFreeFlow({ variables: { id: flowId } });
};
return () => {
Object.keys(files).forEach((node: any) => {
if (files[node]) {
document.body.removeChild(files[node]);
}
});
// clearing all timeouts when component unmounts
const highestTimeoutId: any = setTimeout(() => {});
for (let timeoutId = 0; timeoutId < highestTimeoutId; timeoutId += 1) {
clearTimeout(timeoutId);
}
XMLHttpRequest.prototype.send = xmlSend;
XMLHttpRequest.prototype.open = xmlOpen;
window.fetch = fetch;
};
}
return () => {};
}, [flowId]);
const handlePublishFlow = () => {
publishFlow({ variables: { uuid: match.params.uuid } });
};
const handleCancelFlow = () => {
setPublishDialog(false);
setIsError(false);
setFlowValidation('');
};
const errorMsg = () => (
<div className={styles.DialogError}>
Errors were detected in the flow. Would you like to continue modifying?
<div>
{flowValidation.map((message: any) => (
<div key={message.message}>
<WarningIcon className={styles.ErrorMsgIcon} />
{message.message}
</div>
))}
</div>
</div>
);
if (currentEditDialogBox) {
dialog = (
<DialogBox
title={dialogMessage}
alignButtons="center"
skipCancel
buttonOk="Okay"
handleOk={() => {
setConfirmedNavigation(true);
history.push('/flow');
}}
>
<p className={styles.DialogDescription}>Please try again later or contact the user.</p>
</DialogBox>
);
}
if (publishDialog) {
dialog = (
<DialogBox
title="Ready to publish?"
buttonOk="Publish & Stay"
titleAlign="center"
buttonMiddle="Publish & go back"
handleOk={() => {
setStayOnPublish(true);
handlePublishFlow();
}}
handleCancel={() => handleCancelFlow()}
handleMiddle={() => {
setStayOnPublish(false);
handlePublishFlow();
}}
alignButtons="center"
buttonCancel="Cancel"
additionalTitleStyles={styles.PublishDialogTitle}
>
<p className={styles.DialogDescription}>New changes will be activated for the users</p>
</DialogBox>
);
}
if (IsError) {
dialog = (
<DialogBox
title=""
buttonOk="Publish"
handleOk={() => {
setPublishDialog(false);
setIsError(false);
setPublished(true);
}}
handleCancel={() => handleCancelFlow()}
alignButtons="center"
buttonCancel="Modify"
>
{errorMsg()}
</DialogBox>
);
}
if (published && !IsError) {
setNotification('The flow has been published');
if (!stayOnPublish) {
return <Redirect to="/flow" />;
}
setPublishDialog(false);
setPublished(false);
}
const resetMessage = () => {
setFlowKeyword('');
};
const getFlowKeyword = () => {
const flows = flowName ? flowName.flows : null;
if (flows && flows.length > 0) {
const { isActive, keywords } = flows[0];
if (isActive && keywords.length > 0) {
setFlowKeyword(`draft:${keywords[0]}`);
} else if (keywords.length === 0) {
setFlowKeyword('No keyword found');
} else {
setFlowKeyword('Sorry, the flow is not active');
}
}
};
return (
<>
{dialog}
<div className={styles.ButtonContainer}>
<a
href={FLOWS_HELP_LINK}
className={styles.Link}
target="_blank"
rel="noopener noreferrer"
data-testid="helpButton"
>
<HelpIcon className={styles.HelpIcon} />
</a>
<Button
variant="contained"
color="default"
className={styles.ContainedButton}
onClick={() => {
history.push('/flow');
}}
>
Back
</Button>
<div
className={styles.ExportIcon}
onClick={() => exportFlowMutation({ variables: { id: flowId } })}
aria-hidden="true"
>
<ExportIcon />
</div>
<Button
variant="outlined"
color="primary"
data-testid="saveDraftButton"
className={simulatorId === 0 ? styles.Draft : styles.SimulatorDraft}
onClick={() => {
setNotification('The flow has been saved as draft');
}}
>
Save as draft
</Button>
<Button
variant="contained"
color="primary"
data-testid="button"
className={styles.ContainedButton}
onClick={() => setPublishDialog(true)}
>
Publish
</Button>
</div>
<Simulator
showSimulator={simulatorId > 0}
setSimulatorId={setSimulatorId}
hasResetButton
flowSimulator
message={flowKeyword}
resetMessage={resetMessage}
getFlowKeyword={getFlowKeyword}
/>
{modal}
<Prompt when message={handleBlockedNavigation} />
<div className={styles.FlowContainer}>
<div
className={drawerOpen ? styles.FlowName : styles.FlowNameClosed}
data-testid="flowName"
>
{flowName && (
<>
<IconButton disabled className={styles.Icon}>
<FlowIcon />
</IconButton>
{flowTitle}
</>
)}
</div>
<Button
variant="outlined"
color="primary"
className={drawerOpen ? styles.ResetFlow : styles.ResetClosedDrawer}
data-testid="resetFlow"
onClick={() => setShowResetFlowModal(true)}
aria-hidden="true"
>
<ResetFlowIcon /> Reset flow counts
</Button>
<div id="flow" />
{loading && <Loading />}
</div>
</>
);
}
Example #11
Source File: ApiKeyEditor.tsx From jitsu with MIT License | 4 votes |
ApiKeyEditorComponent: React.FC = props => {
let { id = undefined } = useParams<{ id?: string }>()
if (id) {
id = id.replace("-", ".")
}
const initialApiKey = id ? apiKeysStore.get(id) : apiKeysStore.generateApiKey()
if (!initialApiKey) {
return <CenteredError error={new Error(`Key with id ${id} not found`)} />
}
const [editorObject, setEditorObject] = useState<EditorObject>(getEditorObject(initialApiKey))
const history = useHistory()
const [deleting, setDeleting] = useState(false)
const [saving, setSaving] = useState(false)
const [form] = useForm<any>()
form.setFieldsValue(editorObject)
return (
<div className="flex justify-center w-full">
{form.isFieldsTouched() && !saving && !deleting && <Prompt message={unsavedMessage} />}
<div className="w-full pt-8 px-4" style={{ maxWidth: "1000px" }}>
<Form form={form}>
<FormLayout>
<span
style={
deleting || saving
? {
opacity: "0.5",
pointerEvents: "none",
}
: {}
}
>
<FormField label="Key Name" tooltip="Name of the key" key="comment">
<Form.Item name="comment">
<Input size="large" name="comment" placeholder="Key Name" required={true} />
</Form.Item>
</FormField>
<SecretKey
onGenerate={() => {
setEditorObject({
...editorObject,
jsAuth: apiKeysStore.generateApiToken("js"),
})
}}
formFieldName="jsAuth"
formFieldLabel="Client-side (JS) key"
>
The key that is used for client-side Jitsu libraries (JavaScript, iOS etc). You can consider this key as
'public' since it is visible to any end-user
</SecretKey>
<SecretKey
onGenerate={() => {
setEditorObject({
...editorObject,
serverAuth: apiKeysStore.generateApiToken("s2s"),
})
}}
formFieldName="serverAuth"
formFieldLabel="Server-side key"
>
The key that is user for sending data from backend libraries (python, s2s API etc). Do not publish this
key
</SecretKey>
<FormField
label="HTTP Origins"
tooltip={
<>
If set, only traffic from listed domains will be accepted. Blocking is done via{" "}
<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS">CORS headers</a>. Leave empty for
accept traffic from any domain. Wildcard syntax (<code>*.domain.com</code>) is accepted. Put each
domain on a new line
</>
}
key="js"
>
<Form.Item name="originsText">
<TextArea required={false} size="large" rows={10} name="originsText" />
</Form.Item>
</FormField>
<FormField
label="Connected Destinations"
tooltip={
<>
<NavLink to="/destinations">Destinations</NavLink> that are connected to this particular key
</>
}
key="connectedDestinations"
>
<Form.Item name="connectedDestinations">
<DestinationPicker
allDestinations={destinationsStore.list}
isSelected={dst => dst._onlyKeys.includes(id)}
/>
</Form.Item>
</FormField>
</span>
<FormActions>
{id && (
<Button
loading={deleting}
htmlType="submit"
size="large"
type="default"
danger
onClick={() => {
confirmDelete({
entityName: "api key",
action: async () => {
setDeleting(true)
try {
await flowResult(apiKeysStore.delete(id))
await history.push(projectRoute(apiKeysRoutes.listExact))
} finally {
setDeleting(false)
}
},
})
}}
>
Delete
</Button>
)}
<Button
htmlType="submit"
size="large"
type="default"
onClick={() => {
if (form.isFieldsTouched()) {
if (confirm(unsavedMessage)) {
history.push(projectRoute(apiKeysRoutes.listExact))
}
} else {
history.push(projectRoute(apiKeysRoutes.listExact))
}
}}
>
Cancel
</Button>
<Button
loading={saving}
htmlType="submit"
size="large"
type="primary"
onClick={async () => {
try {
setSaving(true)
const connectedDestinations: string[] = form.getFieldsValue().connectedDestinations || []
let savedKey: ApiKey = getKey(form.getFieldsValue(), initialApiKey)
if (id) {
await flowResult(apiKeysStore.replace({ ...savedKey, uid: id }))
} else {
savedKey = await flowResult(apiKeysStore.add(savedKey))
}
await connectionsHelper.updateDestinationsConnectionsToApiKey(savedKey.uid, connectedDestinations)
history.push(projectRoute(apiKeysRoutes.listExact))
} finally {
setSaving(false)
}
}}
>
Save
</Button>
</FormActions>
</FormLayout>
</Form>
</div>
</div>
)
}
Example #12
Source File: DestinationEditor.tsx From jitsu with MIT License | 4 votes |
DestinationEditor = ({
editorMode,
paramsByProps,
disableForceUpdateOnSave,
onAfterSaveSucceded,
onCancel,
isOnboarding,
}: Props) => {
const history = useHistory()
const forceUpdate = useForceUpdate()
const services = ApplicationServices.get()
const urlParams = useParams<DestinationURLParams>()
const params = paramsByProps || urlParams
const [activeTabKey, setActiveTabKey] = useState<DestinationTabKey>("config")
const [savePopover, switchSavePopover] = useState<boolean>(false)
const [testConnecting, setTestConnecting] = useState<boolean>(false)
const [destinationSaving, setDestinationSaving] = useState<boolean>(false)
const [testConnectingPopover, switchTestConnectingPopover] = useState<boolean>(false)
const sources = sourcesStore.list
const destinationData = useRef<DestinationData>(getDestinationData(params))
const destinationReference = useMemo<Destination | null | undefined>(() => {
if (params.type) {
return destinationsReferenceMap[params.type]
}
return destinationsReferenceMap[getDestinationData(params)._type]
}, [params.type, params.id])
if (!destinationReference) {
return <DestinationNotFound destinationId={params.id} />
}
const submittedOnce = useRef<boolean>(false)
const handleUseLibrary = async (newMappings: DestinationMapping, newTableName?: string) => {
destinationData.current = {
...destinationData.current,
_formData: {
...destinationData.current._formData,
tableName: newTableName ? newTableName : destinationData.current._formData?.tableName,
},
_mappings: newMappings,
}
const { form: mappingsForm } = destinationsTabs[2]
const { form: configForm } = destinationsTabs[0]
await mappingsForm.setFieldsValue({
"_mappings._mappings": newMappings._mappings,
"_mappings._keepUnmappedFields": newMappings._keepUnmappedFields,
})
destinationsTabs[2].touched = true
if (newTableName) {
await configForm.setFieldsValue({
"_formData.tableName": newTableName,
})
destinationsTabs[0].touched = true
}
await forceUpdate()
actionNotification.success("Mappings library has been successfully set")
}
const validateTabForm = useCallback(
async (tab: Tab) => {
const tabForm = tab.form
try {
if (tab.key === "sources") {
const _sources = tabForm.getFieldsValue()?._sources
if (!_sources) {
tab.errorsCount = 1
}
}
tab.errorsCount = 0
return await tabForm.validateFields()
} catch (errors) {
// ToDo: check errors count for fields with few validation rules
tab.errorsCount = errors.errorFields?.length
return null
} finally {
forceUpdate()
}
},
[forceUpdate]
)
const configForm = Form.useForm()[0]
const hideMapping =
params.standalone == "true" ||
isOnboarding ||
editorMode === "add" ||
(destinationsReferenceMap[destinationReference.id].defaultTransform.length > 0 &&
!destinationData.current._mappings?._mappings) ||
!destinationData.current._mappings?._mappings
let mappingForm = undefined
if (!hideMapping) {
mappingForm = Form.useForm()[0]
}
const tabsInitialData: Tab<DestinationTabKey>[] = [
{
key: "config",
name: "Connection Properties",
getComponent: (form: FormInstance) => (
<DestinationEditorConfig
form={form}
destinationReference={destinationReference}
destinationData={destinationData.current}
handleTouchAnyField={validateAndTouchField(0)}
/>
),
form: configForm,
touched: false,
},
{
key: "transform",
name: "Transform",
getComponent: (form: FormInstance) => (
<DestinationEditorTransform
form={form}
configForm={configForm}
mappingForm={mappingForm}
destinationReference={destinationReference}
destinationData={destinationData.current}
handleTouchAnyField={validateAndTouchField(1)}
/>
),
form: Form.useForm()[0],
touched: false,
isHidden: params.standalone == "true",
},
{
key: "mappings",
name: "Mappings (Deprecated)",
isDisabled: destinationData.current["_transform_enabled"],
getComponent: (form: FormInstance) => (
<DestinationEditorMappings
form={form}
initialValues={destinationData.current._mappings}
handleTouchAnyField={validateAndTouchField(2)}
handleDataUpdate={handleUseLibrary}
/>
),
form: mappingForm,
touched: false,
isHidden: hideMapping,
},
{
key: "sources",
name: "Linked Connectors & API Keys",
getComponent: (form: FormInstance) => (
<DestinationEditorConnectors
form={form}
initialValues={destinationData.current}
destination={destinationReference}
handleTouchAnyField={validateAndTouchField(3)}
/>
),
form: Form.useForm()[0],
errorsLevel: "warning",
touched: false,
isHidden: params.standalone == "true",
},
]
const [destinationsTabs, setDestinationsTabs] = useState<Tab<DestinationTabKey>[]>(tabsInitialData)
const validateAndTouchField = useCallback(
(index: number) => (value: boolean) => {
destinationsTabs[index].touched = value === undefined ? true : value
setDestinationsTabs(oldTabs => {
let tab = oldTabs[index]
let oldErrorsCount = tab.errorsCount
let newErrorsCount = tab.form.getFieldsError().filter(a => a.errors?.length > 0).length
if (newErrorsCount != oldErrorsCount) {
tab.errorsCount = newErrorsCount
}
if (
oldTabs[1].form.getFieldValue("_transform_enabled") !== oldTabs[2].isDisabled ||
newErrorsCount != oldErrorsCount
) {
const newTabs = [
...oldTabs.slice(0, 2),
{ ...oldTabs[2], isDisabled: oldTabs[1].form.getFieldValue("_transform_enabled") },
...oldTabs.slice(3),
]
if (newErrorsCount != oldErrorsCount) {
newTabs[index].errorsCount = newErrorsCount
}
return newTabs
} else {
return oldTabs
}
})
},
[validateTabForm, destinationsTabs, setDestinationsTabs]
)
const handleCancel = useCallback(() => {
onCancel ? onCancel() : history.push(projectRoute(destinationPageRoutes.root))
}, [history, onCancel])
const handleViewStatistics = () =>
history.push(
projectRoute(destinationPageRoutes.statisticsExact, {
id: destinationData.current._id,
})
)
const testConnectingPopoverClose = useCallback(() => switchTestConnectingPopover(false), [])
const savePopoverClose = useCallback(() => switchSavePopover(false), [])
const handleTestConnection = useCallback(async () => {
setTestConnecting(true)
const tab = destinationsTabs[0]
try {
const config = await validateTabForm(tab)
const values = makeObjectFromFieldsValues<DestinationData>(config)
destinationData.current._formData = values._formData
destinationData.current._package = values._package
destinationData.current._super_type = values._super_type
await destinationEditorUtils.testConnection(destinationData.current)
} catch (error) {
switchTestConnectingPopover(true)
} finally {
setTestConnecting(false)
forceUpdate()
}
}, [validateTabForm, forceUpdate])
const handleSaveDestination = useCallback(() => {
submittedOnce.current = true
setDestinationSaving(true)
Promise.all(destinationsTabs.filter((tab: Tab) => !!tab.form).map((tab: Tab) => validateTabForm(tab)))
.then(async allValues => {
destinationData.current = {
...destinationData.current,
...allValues.reduce((result: any, current: any) => {
return {
...result,
...makeObjectFromFieldsValues(current),
}
}, {}),
}
try {
await destinationEditorUtils.testConnection(destinationData.current, true)
let savedDestinationData: DestinationData = destinationData.current
if (editorMode === "add") {
savedDestinationData = await flowResult(destinationsStore.add(destinationData.current))
}
if (editorMode === "edit") {
await flowResult(destinationsStore.replace(destinationData.current))
}
await connectionsHelper.updateSourcesConnectionsToDestination(
savedDestinationData._uid,
savedDestinationData._sources || []
)
destinationsTabs.forEach((tab: Tab) => (tab.touched = false))
if (savedDestinationData._connectionTestOk) {
if (editorMode === "add") actionNotification.success(`New ${savedDestinationData._type} has been added!`)
if (editorMode === "edit") actionNotification.success(`${savedDestinationData._type} has been saved!`)
} else {
actionNotification.warn(
`${savedDestinationData._type} has been saved, but test has failed with '${firstToLower(
savedDestinationData._connectionErrorMessage
)}'. Data will not be piped to this destination`
)
}
onAfterSaveSucceded ? onAfterSaveSucceded() : history.push(projectRoute(destinationPageRoutes.root))
} catch (errors) {}
})
.catch(() => {
switchSavePopover(true)
})
.finally(() => {
setDestinationSaving(false)
!disableForceUpdateOnSave && forceUpdate()
})
}, [
destinationsTabs,
destinationData,
sources,
history,
validateTabForm,
forceUpdate,
editorMode,
services.activeProject.id,
services.storageService,
])
const connectedSourcesNum = sources.filter(src =>
(src.destinations || []).includes(destinationData.current._uid)
).length
const isAbleToConnectItems = (): boolean =>
editorMode === "edit" &&
connectedSourcesNum === 0 &&
!destinationData.current?._onlyKeys?.length &&
!destinationsReferenceMap[params.type]?.hidden
useEffect(() => {
let breadcrumbs = []
if (!params.standalone) {
breadcrumbs.push({
title: "Destinations",
link: projectRoute(destinationPageRoutes.root),
})
}
breadcrumbs.push({
title: (
<PageHeader
title={destinationReference?.displayName ?? "Not Found"}
icon={destinationReference?.ui.icon}
mode={params.standalone ? "edit" : editorMode}
/>
),
})
currentPageHeaderStore.setBreadcrumbs(...breadcrumbs)
}, [destinationReference])
return (
<>
<div className={cn("flex flex-col items-stretch flex-auto", styles.wrapper)}>
<div className={styles.mainArea} id="dst-editor-tabs">
{isAbleToConnectItems() && (
<Card className={styles.linkedWarning}>
<WarningOutlined className={styles.warningIcon} />
<article>
This destination is not linked to any API keys or Connector. You{" "}
<span className={styles.pseudoLink} onClick={() => setActiveTabKey("sources")}>
can link the destination here
</span>
.
</article>
</Card>
)}
<TabsConfigurator
type="card"
className={styles.tabCard}
tabsList={destinationsTabs}
activeTabKey={activeTabKey}
onTabChange={setActiveTabKey}
tabBarExtraContent={
!params.standalone &&
!isOnboarding &&
editorMode == "edit" && (
<Button
size="large"
className="mr-3"
type="link"
onClick={handleViewStatistics}
icon={<AreaChartOutlined />}
>
Statistics
</Button>
)
}
/>
</div>
<div className="flex-shrink border-t py-2">
<EditorButtons
save={{
isRequestPending: destinationSaving,
isPopoverVisible: savePopover && destinationsTabs.some((tab: Tab) => tab.errorsCount > 0),
handlePress: handleSaveDestination,
handlePopoverClose: savePopoverClose,
titleText: "Destination editor errors",
tabsList: destinationsTabs,
}}
test={{
isRequestPending: testConnecting,
isPopoverVisible: testConnectingPopover && destinationsTabs[0].errorsCount > 0,
handlePress: handleTestConnection,
handlePopoverClose: testConnectingPopoverClose,
titleText: "Connection Properties errors",
tabsList: [destinationsTabs[0]],
}}
handleCancel={params.standalone ? undefined : handleCancel}
/>
</div>
</div>
<Prompt message={destinationEditorUtils.getPromptMessage(destinationsTabs)} />
</>
)
}