Skip to content

Commit

Permalink
Poll the server for conflicting content on the Editor page
Browse files Browse the repository at this point in the history
  • Loading branch information
EricWittmann committed Nov 11, 2024
1 parent d29f5fe commit 9b4b960
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 29 deletions.
2 changes: 1 addition & 1 deletion ui/ui-app/src/app/components/common/IfRegistryFeature.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const IfRegistryFeature: FunctionComponent<IfRegistryFeatureProps> = (pro
const config = useConfigService();

const accept = () => {
const features: any = config.getApicurioRegistryConfig().features;
const features: any = config.getApicurioRegistryConfig().features || {};
const featureValue: any = features[props.feature];
if (props.is !== undefined) {
return featureValue === props.is;
Expand Down
23 changes: 21 additions & 2 deletions ui/ui-app/src/app/pages/editor/EditorPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ export const EditorPage: FunctionComponent<EditorPageProps> = () => {
const [isPleaseWaitModalOpen, setPleaseWaitModalOpen] = useState(false);
const [isConfirmOverwriteModalOpen, setConfirmOverwriteModalOpen] = useState(false);
const [pleaseWaitMessage, setPleaseWaitMessage] = useState("");
const [intervalId, setIntervalId] = useState<any>();
const [isContentConflicting, setIsContentConflicting] = useState(false);

const { groupId, draftId, version } = useParams();

Expand All @@ -85,15 +87,21 @@ export const EditorPage: FunctionComponent<EditorPageProps> = () => {
const createLoaders = (): Promise<any>[] => {
return [
drafts.getDraft(groupId as string, draftId as string, version as string)
.then(setDraft)
.then(d => {
setDraft(d);

// Poll the server for new content every 60s. If the content has been updated on
// the server then we have a conflict that we need to report to the user.
setIntervalId(setInterval(detectContentConflict, 60000));
})
.catch(error => {
setPageError(toPageError(error, "Error loading page data."));
}),
drafts.getDraftContent(groupId as string, draftId as string, version as string).then(content => {
setOriginalContent(content.content);
setCurrentContent(content.content);
setDraftContent(content);
})
}),
];
};

Expand All @@ -102,6 +110,7 @@ export const EditorPage: FunctionComponent<EditorPageProps> = () => {
// Cleanup any possible event listener we might still have registered
return () => {
window.removeEventListener("beforeunload", onBeforeUnload);
clearInterval(intervalId);
};
}, []);

Expand Down Expand Up @@ -134,6 +143,15 @@ export const EditorPage: FunctionComponent<EditorPageProps> = () => {
setPleaseWaitMessage(message);
};

const detectContentConflict = (): void => {
drafts.getDraft(groupId as string, draftId as string, version as string).then(currentDraft => {
if (currentDraft.contentId !== draft.contentId) {
console.debug(`[EditorPage] Detected Draft content conflict. Expected '${draft.contentId}' but found '${currentDraft.contentId}'.'`);
setIsContentConflicting(true);
}
});
};

const updateDraftMetadata = (): void => {
drafts.getDraft(groupId as string, draftId as string, version as string).then(setDraft);
};
Expand Down Expand Up @@ -254,6 +272,7 @@ export const EditorPage: FunctionComponent<EditorPageProps> = () => {
<EditorContext
draft={draft}
dirty={isDirty}
contentConflict={isContentConflicting}
onSave={onSave}
onFormat={onFormat}
onDownload={onDownload}
Expand Down
23 changes: 23 additions & 0 deletions ui/ui-app/src/app/pages/editor/components/EditorContext.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
flex-grow: 1;
line-height: 34px;
}
.editor-context .editor-context-conflict {
line-height: 34px;
}
.editor-context .editor-context-last-modified {
margin-left: 10px;
line-height: 34px;
Expand Down Expand Up @@ -47,3 +50,23 @@
#action-toggle {
padding-right: 5px;
}

@keyframes pulse {
0% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.2); /* Slightly larger */
opacity: 0.8; /* Slightly faded */
color: #ea6b1b;
}
100% {
transform: scale(1);
opacity: 1;
}
}

.icon-pulse {
animation: pulse 1.5s infinite ease-in-out;
}
71 changes: 45 additions & 26 deletions ui/ui-app/src/app/pages/editor/components/EditorContext.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import React, { FunctionComponent } from "react";
import { FunctionComponent } from "react";
import "./EditorContext.css";
import { Breadcrumb, BreadcrumbItem, Button } from "@patternfly/react-core";
import { Breadcrumb, BreadcrumbItem, Button, Icon, Popover } from "@patternfly/react-core";
import { FromNow, If, ObjectDropdown } from "@apicurio/common-ui-components";
import { ArtifactTypes } from "@models/common";
import { Draft } from "@models/drafts";
import { Link } from "react-router-dom";
import { AppNavigationService, useAppNavigation } from "@services/useAppNavigation.ts";
import { ExclamationTriangleIcon } from "@patternfly/react-icons";

/**
* Properties
*/
export type EditorContextProps = {
draft: Draft;
dirty: boolean;
contentConflict: boolean;
onSave: () => void;
onFormat: () => void;
onDownload: () => void;
Expand Down Expand Up @@ -74,33 +76,50 @@ export const EditorContext: FunctionComponent<EditorContextProps> = (props: Edit
</Breadcrumb>
);

const contentConflictHeader = (
<span>Content conflict</span>
);
const contentConflictComponent = (
<div>The content of this Draft has been saved by someone else since you opened this editor!</div>
);

return (
<React.Fragment>
<div className="editor-context">
<div className="editor-context-breadcrumbs" children={breadcrumbs} />
<If condition={props.draft.modifiedOn !== undefined}>
<div className="editor-context-last-modified">
<span>Last modified:</span>
<FromNow date={props.draft.modifiedOn}/>
</div>
</If>
<div className="editor-context-actions">
<ObjectDropdown
label="Actions"
items={menuItems}
testId="select-actions"
onSelect={item => item.onSelect()}
noSelectionLabel="Actions"
itemToTestId={item => item.testId}
itemIsVisible={item => !item.isVisible || item.isVisible()}
itemIsDivider={item => item.isDivider}
itemIsDisabled={item => item.isDisabled === undefined ? false : item.isDisabled()}
itemToString={item => item.label} />
<div className="editor-context">
<div className="editor-context-breadcrumbs" children={breadcrumbs} />
<If condition={props.contentConflict}>
<div className="editor-context-conflict">
<Icon status="warning">
<Popover
triggerAction="hover"
headerContent={contentConflictHeader}
bodyContent={contentConflictComponent}>
<ExclamationTriangleIcon className="icon-pulse" />
</Popover>
</Icon>
</div>
<div className="editor-context-save">
<Button className="btn-save" variant="primary" onClick={() => props.onSave()} isDisabled={!props.dirty}>Save</Button>
</If>
<If condition={props.draft.modifiedOn !== undefined}>
<div className="editor-context-last-modified">
<span>Last modified:</span>
<FromNow date={props.draft.modifiedOn}/>
</div>
</If>
<div className="editor-context-actions">
<ObjectDropdown
label="Actions"
items={menuItems}
testId="select-actions"
onSelect={item => item.onSelect()}
noSelectionLabel="Actions"
itemToTestId={item => item.testId}
itemIsVisible={item => !item.isVisible || item.isVisible()}
itemIsDivider={item => item.isDivider}
itemIsDisabled={item => item.isDisabled === undefined ? false : item.isDisabled()}
itemToString={item => item.label} />
</div>
<div className="editor-context-save">
<Button className="btn-save" variant="primary" onClick={() => props.onSave()} isDisabled={!props.dirty}>Save</Button>
</div>
</React.Fragment>
</div>
);
};
14 changes: 14 additions & 0 deletions ui/ui-app/src/services/useConfigService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,20 @@ export class ConfigServiceImpl implements ConfigService {
return fetch(registryConfigEndpoint).then(response => {
console.info("[ConfigService] Loaded Registry UI config: ", response);
registryConfig = JSON.parse(response as string);
}).catch(() => {
registryConfig = {
auth: {
type: "none",
obacEnabled: false,
rbacEnabled: false,
},
features: {
deleteVersion: false,
deleteArtifact: false,
deleteGroup: false,
draftMutability: false
},
};
});
}

Expand Down

0 comments on commit 9b4b960

Please sign in to comment.