Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Loading text analysis from WS and displaying long-running tasks #512

Merged
Merged
9 changes: 7 additions & 2 deletions src/WebSocketApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,12 @@ export type StompSessionProviderType = (
*/
export const WebSocketWrapper: React.FC<{
Provider?: StompSessionProviderType;
}> = ({ children, Provider = StompSessionProvider }) => {
loadDispatchers?: boolean;
}> = ({
children,
Provider = StompSessionProvider,
loadDispatchers = true,
}) => {
const [securityToken, setSecurityToken] = useState<string>("");

useEffect(() => {
Expand Down Expand Up @@ -115,7 +120,7 @@ export const WebSocketWrapper: React.FC<{
console.warn("Unhandled STOMP receipt", receipt)
}
>
{registerDispatchers()}
{loadDispatchers && registerDispatchers()}
{children}
</Provider>
);
Expand Down
6 changes: 5 additions & 1 deletion src/__tests__/environment/Environment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Provider } from "react-redux";
import configureMockStore from "redux-mock-store";
import thunk from "redux-thunk";
import TermItState from "../../model/TermItState";
import { mock as stompMock } from "react-stomp-hooks";
import { mock as stompMock, StompSessionProvider } from "react-stomp-hooks";
// @ts-ignore
import TimeAgo from "javascript-time-ago";
import IntlData from "../../model/IntlData";
Expand Down Expand Up @@ -91,3 +91,7 @@ export function withWebSocket(node: ReactElement) {
</WebSocketWrapper>
);
}

export const webSocketProviderWrappingComponentOptions = {
wrappingComponent: StompSessionProvider,
};
1 change: 1 addition & 0 deletions src/action/ActionType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ enum ActionType {
REMOVE_CRUMB = "REMOVE_CRUMB",

DOES_USERNAME_EXISTS = "DOES_USERNAME_EXISTS",
LONG_RUNNING_TASKS_UPDATE = "LONG_RUNNING_TASKS_UPDATE",
}

export default ActionType;
12 changes: 1 addition & 11 deletions src/action/AsyncActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -708,17 +708,7 @@ export function executeFileTextAnalysis(fileIri: IRI, vocabularyIri: string) {
params(reqParams)
)
.then(() => {
dispatch(asyncActionSuccess(action));
return dispatch(
publishMessage(
new Message(
{
messageId: "file.text-analysis.finished.message",
},
MessageType.SUCCESS
)
)
);
return dispatch(asyncActionSuccess(action));
})
.catch((error: ErrorData) => {
dispatch(asyncActionFailure(action, error));
Expand Down
19 changes: 0 additions & 19 deletions src/action/__tests__/AsyncActions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -608,25 +608,6 @@ describe("Async actions", () => {
expect((found as MessageAction).message.type).toBe(MessageType.ERROR);
});
});

it("publishes message on success", () => {
Ajax.put = jest.fn().mockImplementation(() => Promise.resolve("Success"));
return Promise.resolve(
(store.dispatch as ThunkDispatch)(
executeFileTextAnalysis(
VocabularyUtils.create(file.iri),
Generator.generateUri()
)
)
).then(() => {
const actions: Action[] = store.getActions();
const found = actions.find(
(a) => a.type === ActionType.PUBLISH_MESSAGE
);
expect(found).toBeDefined();
expect((found as MessageAction).message.type).toBe(MessageType.SUCCESS);
});
});
});

describe("load terms", () => {
Expand Down
8 changes: 6 additions & 2 deletions src/component/MainView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import Routing from "src/util/Routing";
import { Configuration, DEFAULT_CONFIGURATION } from "../model/Configuration";
import Breadcrumbs from "./breadcrumb/Breadcrumbs";
import { loadTermStates } from "../action/AsyncActions";
import { LongRunningTasksStatus } from "./main/LongRunningTasksStatus";

const AdministrationRoute = React.lazy(
() => import("./administration/AdministrationRoute")
Expand Down Expand Up @@ -142,8 +143,11 @@ export class MainView extends React.Component<MainViewProps, MainViewState> {
<NavbarSearch navbar={true} />
</Nav>

<Nav navbar={true} className="nav-menu-user flex-row-reverse">
<UserDropdown dark={false} />
<Nav>
<LongRunningTasksStatus />
<Nav navbar={true} className="nav-menu-user flex-row-reverse">
<UserDropdown dark={false} />
</Nav>
</Nav>
</Navbar>
)}
Expand Down
61 changes: 45 additions & 16 deletions src/component/annotator/TextAnalysisInvocationButton.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import * as React from "react";
import { injectIntl } from "react-intl";
import withI18n, { HasI18n } from "../hoc/withI18n";
import withInjectableLoading, {
InjectsLoading,
} from "../hoc/withInjectableLoading";
import { GoClippy } from "react-icons/go";
import { Button } from "reactstrap";
import { connect } from "react-redux";
import { ThunkDispatch } from "../../util/Types";
import { executeFileTextAnalysis } from "../../action/AsyncActions";
import { publishNotification } from "../../action/SyncActions";
import NotificationType from "../../model/NotificationType";
import ResourceSelectVocabulary from "../resource/ResourceSelectVocabulary";
import Vocabulary from "../../model/Vocabulary";
import { IRI } from "../../util/VocabularyUtils";
import { IRI, IRIImpl } from "../../util/VocabularyUtils";
import { IMessage, withSubscription } from "react-stomp-hooks";
import Constants from "../../util/Constants";
import { publishMessage, publishNotification } from "../../action/SyncActions";
import NotificationType from "../../model/NotificationType";
import Message from "../../model/Message";
import MessageType from "../../model/MessageType";

interface TextAnalysisInvocationButtonProps extends HasI18n, InjectsLoading {
interface TextAnalysisInvocationButtonProps extends HasI18n {
id?: string;
fileIri: IRI;
defaultVocabularyIri?: string;
Expand All @@ -32,7 +33,7 @@ export class TextAnalysisInvocationButton extends React.Component<
TextAnalysisInvocationButtonProps,
TextAnalysisInvocationButtonState
> {
constructor(props: InjectsLoading & TextAnalysisInvocationButtonProps) {
constructor(props: TextAnalysisInvocationButtonProps) {
super(props);
this.state = { showVocabularySelector: false };
}
Expand All @@ -42,11 +43,7 @@ export class TextAnalysisInvocationButton extends React.Component<
};

private invokeTextAnalysis(fileIri: IRI, vocabularyIri: string) {
this.props.loadingOn();
this.props.executeTextAnalysis(fileIri, vocabularyIri).then(() => {
this.props.loadingOff();
this.props.notifyAnalysisFinish();
});
this.props.executeTextAnalysis(fileIri, vocabularyIri);
}

public onVocabularySelect = (vocabulary: Vocabulary | null) => {
Expand All @@ -61,6 +58,18 @@ export class TextAnalysisInvocationButton extends React.Component<
this.setState({ showVocabularySelector: false });
};

public onMessage(message: IMessage) {
if (!message?.body) {
return;
}
if (
message.body.substring(1, message.body.length - 1) ===
IRIImpl.toString(this.props.fileIri)
) {
this.props.notifyAnalysisFinish();
}
}

public render() {
const i18n = this.props.i18n;
return (
Expand Down Expand Up @@ -92,11 +101,31 @@ export default connect(undefined, (dispatch: ThunkDispatch) => {
return {
executeTextAnalysis: (fileIri: IRI, vocabularyIri: string) =>
dispatch(executeFileTextAnalysis(fileIri, vocabularyIri)),
notifyAnalysisFinish: () =>
notifyAnalysisFinish: () => {
dispatch(
publishMessage(
new Message(
{
messageId: "file.text-analysis.finished.message",
},
MessageType.SUCCESS
)
)
);
dispatch(
publishNotification({
source: { type: NotificationType.TEXT_ANALYSIS_FINISHED },
})
),
);
},
};
})(injectIntl(withI18n(withInjectableLoading(TextAnalysisInvocationButton))));
})(
injectIntl(
withI18n(
withSubscription(
TextAnalysisInvocationButton,
Constants.WEBSOCKET_ENDPOINT.VOCABULARIES_TEXT_ANALYSIS_FINISHED_FILE
)
)
)
);
111 changes: 61 additions & 50 deletions src/component/annotator/__tests__/Annotator.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
mockWindowSelection,
mountWithIntl,
withWebSocket,
} from "../../../__tests__/environment/Environment";
import { Element } from "domhandler";
import { AnnotationSpanProps, Annotator } from "../Annotator";
Expand Down Expand Up @@ -100,16 +101,18 @@ describe("Annotator", () => {

it("renders body of provided html content", () => {
const wrapper = mountWithIntl(
<MemoryRouter>
<Annotator
fileIri={fileIri}
vocabularyIri={vocabularyIri}
{...mockedCallbackProps}
{...stateProps}
initialHtml={generalHtmlContent}
{...intlFunctions()}
/>
</MemoryRouter>
withWebSocket(
<MemoryRouter>
<Annotator
fileIri={fileIri}
vocabularyIri={vocabularyIri}
{...mockedCallbackProps}
{...stateProps}
initialHtml={generalHtmlContent}
{...intlFunctions()}
/>
</MemoryRouter>
)
);

expect(wrapper.html().includes(sampleContent)).toBe(true);
Expand All @@ -121,16 +124,18 @@ describe("Annotator", () => {
);

const wrapper = mountWithIntl(
<MemoryRouter>
<Annotator
fileIri={fileIri}
vocabularyIri={vocabularyIri}
{...mockedCallbackProps}
{...stateProps}
initialHtml={htmlContent}
{...intlFunctions()}
/>
</MemoryRouter>
withWebSocket(
<MemoryRouter>
<Annotator
fileIri={fileIri}
vocabularyIri={vocabularyIri}
{...mockedCallbackProps}
{...stateProps}
initialHtml={htmlContent}
{...intlFunctions()}
/>
</MemoryRouter>
)
);
const sampleOutput =
'This is a <a data-href="https://example.org/link">link</a>';
Expand All @@ -142,16 +147,18 @@ describe("Annotator", () => {
createAnnotation(suggestedOccProps, "města")
);
const wrapper = mountWithIntlAttached(
<MemoryRouter>
<Annotator
fileIri={fileIri}
vocabularyIri={vocabularyIri}
{...mockedCallbackProps}
{...stateProps}
initialHtml={htmlWithOccurrence}
{...intlFunctions()}
/>
</MemoryRouter>
withWebSocket(
<MemoryRouter>
<Annotator
fileIri={fileIri}
vocabularyIri={vocabularyIri}
{...mockedCallbackProps}
{...stateProps}
initialHtml={htmlWithOccurrence}
{...intlFunctions()}
/>
</MemoryRouter>
)
);

const constructedAnnProps = wrapper.find(Annotation).props();
Expand Down Expand Up @@ -516,16 +523,18 @@ describe("Annotator", () => {
});
HtmlDomUtils.isInPopup = jest.fn().mockReturnValue(false);
const wrapper = mountWithIntl(
<MemoryRouter>
<Annotator
fileIri={fileIri}
vocabularyIri={vocabularyIri}
{...mockedCallbackProps}
{...stateProps}
initialHtml={generalHtmlContent}
{...intlFunctions()}
/>
</MemoryRouter>
withWebSocket(
<MemoryRouter>
<Annotator
fileIri={fileIri}
vocabularyIri={vocabularyIri}
{...mockedCallbackProps}
{...stateProps}
initialHtml={generalHtmlContent}
{...intlFunctions()}
/>
</MemoryRouter>
)
);
wrapper.find("#annotator").simulate("mouseUp");
wrapper.update();
Expand All @@ -537,16 +546,18 @@ describe("Annotator", () => {
getPropertyValue: () => "16px",
});
const wrapper = mountWithIntl(
<MemoryRouter>
<Annotator
fileIri={fileIri}
vocabularyIri={vocabularyIri}
{...mockedCallbackProps}
{...stateProps}
initialHtml={generalHtmlContent}
{...intlFunctions()}
/>
</MemoryRouter>
withWebSocket(
<MemoryRouter>
<Annotator
fileIri={fileIri}
vocabularyIri={vocabularyIri}
{...mockedCallbackProps}
{...stateProps}
initialHtml={generalHtmlContent}
{...intlFunctions()}
/>
</MemoryRouter>
)
);
wrapper.find("#annotator").simulate("mouseUp");
expect(wrapper.find(SelectionPurposeDialog).props().show).toBeTruthy();
Expand Down
Loading