@apollo-annotation/jbrowse-plugin-apollo 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -0
- package/dist/index.esm.js +10248 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +7 -0
- package/dist/jbrowse-plugin-apollo.cjs.development.js +10298 -0
- package/dist/jbrowse-plugin-apollo.cjs.development.js.map +1 -0
- package/dist/jbrowse-plugin-apollo.cjs.production.min.js +2 -0
- package/dist/jbrowse-plugin-apollo.cjs.production.min.js.map +1 -0
- package/dist/jbrowse-plugin-apollo.umd.development.js +46957 -0
- package/dist/jbrowse-plugin-apollo.umd.development.js.map +1 -0
- package/dist/jbrowse-plugin-apollo.umd.production.min.js +2 -0
- package/dist/jbrowse-plugin-apollo.umd.production.min.js.map +1 -0
- package/package.json +130 -0
- package/src/ApolloInternetAccount/addMenuItems.ts +94 -0
- package/src/ApolloInternetAccount/components/AuthTypeSelector.tsx +121 -0
- package/src/ApolloInternetAccount/components/LoginButtons.tsx +62 -0
- package/src/ApolloInternetAccount/components/LoginIcons.tsx +74 -0
- package/src/ApolloInternetAccount/configSchema.ts +26 -0
- package/src/ApolloInternetAccount/index.ts +2 -0
- package/src/ApolloInternetAccount/model.ts +448 -0
- package/src/ApolloJobModel.ts +117 -0
- package/src/ApolloSequenceAdapter/ApolloSequenceAdapter.ts +186 -0
- package/src/ApolloSequenceAdapter/configSchema.ts +12 -0
- package/src/ApolloSequenceAdapter/index.ts +21 -0
- package/src/ApolloSixFrameRenderer/ApolloSixFrameRenderer.tsx +12 -0
- package/src/ApolloSixFrameRenderer/components/ApolloRendering.tsx +692 -0
- package/src/ApolloSixFrameRenderer/configSchema.ts +7 -0
- package/src/ApolloSixFrameRenderer/index.ts +3 -0
- package/src/ApolloTextSearchAdapter/ApolloTextSearchAdapter.ts +64 -0
- package/src/ApolloTextSearchAdapter/configSchema.ts +24 -0
- package/src/ApolloTextSearchAdapter/index.ts +18 -0
- package/src/BackendDrivers/BackendDriver.ts +31 -0
- package/src/BackendDrivers/CollaborationServerDriver.ts +318 -0
- package/src/BackendDrivers/DesktopFileDriver.ts +170 -0
- package/src/BackendDrivers/InMemoryFileDriver.ts +76 -0
- package/src/BackendDrivers/index.ts +4 -0
- package/src/ChangeManager.ts +148 -0
- package/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +248 -0
- package/src/LinearApolloDisplay/components/index.ts +1 -0
- package/src/LinearApolloDisplay/configSchema.ts +16 -0
- package/src/LinearApolloDisplay/glyphs/BoxGlyph.ts +422 -0
- package/src/LinearApolloDisplay/glyphs/CanonicalGeneGlyph.ts +1191 -0
- package/src/LinearApolloDisplay/glyphs/GenericChildGlyph.ts +151 -0
- package/src/LinearApolloDisplay/glyphs/Glyph.ts +382 -0
- package/src/LinearApolloDisplay/glyphs/ImplicitExonGeneGlyph.ts +697 -0
- package/src/LinearApolloDisplay/glyphs/index.ts +4 -0
- package/src/LinearApolloDisplay/index.ts +2 -0
- package/src/LinearApolloDisplay/stateModel/base.ts +146 -0
- package/src/LinearApolloDisplay/stateModel/getGlyph.ts +39 -0
- package/src/LinearApolloDisplay/stateModel/glyphs.ts +45 -0
- package/src/LinearApolloDisplay/stateModel/index.ts +20 -0
- package/src/LinearApolloDisplay/stateModel/layouts.ts +230 -0
- package/src/LinearApolloDisplay/stateModel/mouseEvents.ts +513 -0
- package/src/LinearApolloDisplay/stateModel/rendering.ts +441 -0
- package/src/LinearApolloDisplay/stateModel/trackHeightMixin.ts +43 -0
- package/src/LinearApolloDisplay/types.ts +1 -0
- package/src/OntologyManager/OntologyStore/__snapshots__/fulltext.test.ts.snap +208 -0
- package/src/OntologyManager/OntologyStore/__snapshots__/index.test.ts.snap +18846 -0
- package/src/OntologyManager/OntologyStore/fulltext-stopwords.ts +137 -0
- package/src/OntologyManager/OntologyStore/fulltext.test.ts +94 -0
- package/src/OntologyManager/OntologyStore/fulltext.ts +264 -0
- package/src/OntologyManager/OntologyStore/index.test.ts +130 -0
- package/src/OntologyManager/OntologyStore/index.ts +526 -0
- package/src/OntologyManager/OntologyStore/indexeddb-schema.ts +89 -0
- package/src/OntologyManager/OntologyStore/indexeddb-storage.ts +180 -0
- package/src/OntologyManager/OntologyStore/obo-graph-json-schema.ts +110 -0
- package/src/OntologyManager/OntologyStore/prefixes.ts +35 -0
- package/src/OntologyManager/index.ts +173 -0
- package/src/SixFrameFeatureDisplay/components/TrackLines.tsx +19 -0
- package/src/SixFrameFeatureDisplay/components/index.ts +1 -0
- package/src/SixFrameFeatureDisplay/configSchema.ts +21 -0
- package/src/SixFrameFeatureDisplay/index.ts +2 -0
- package/src/SixFrameFeatureDisplay/stateModel.ts +413 -0
- package/src/TabularEditor/HybridGrid/ChangeHandling.ts +88 -0
- package/src/TabularEditor/HybridGrid/Feature.tsx +346 -0
- package/src/TabularEditor/HybridGrid/FeatureAttributes.tsx +34 -0
- package/src/TabularEditor/HybridGrid/Highlight.tsx +40 -0
- package/src/TabularEditor/HybridGrid/HybridGrid.tsx +138 -0
- package/src/TabularEditor/HybridGrid/NumberCell.tsx +77 -0
- package/src/TabularEditor/HybridGrid/ToolBar.tsx +59 -0
- package/src/TabularEditor/HybridGrid/featureContextMenuItems.ts +119 -0
- package/src/TabularEditor/HybridGrid/index.ts +1 -0
- package/src/TabularEditor/TabularEditorPane.tsx +34 -0
- package/src/TabularEditor/index.ts +3 -0
- package/src/TabularEditor/model.ts +44 -0
- package/src/TabularEditor/types.ts +3 -0
- package/src/components/AddAssembly.tsx +464 -0
- package/src/components/AddChildFeature.tsx +247 -0
- package/src/components/AddFeature.tsx +252 -0
- package/src/components/CopyFeature.tsx +328 -0
- package/src/components/DeleteAssembly.tsx +185 -0
- package/src/components/DeleteFeature.tsx +90 -0
- package/src/components/Dialog.tsx +47 -0
- package/src/components/DownloadGFF3.tsx +213 -0
- package/src/components/ImportFeatures.tsx +295 -0
- package/src/components/ManageChecks.tsx +280 -0
- package/src/components/ManageUsers.tsx +218 -0
- package/src/components/ModifyFeatureAttribute.tsx +457 -0
- package/src/components/OntologyTermAutocomplete.tsx +240 -0
- package/src/components/OntologyTermMultiSelect.tsx +349 -0
- package/src/components/OpenLocalFile.tsx +178 -0
- package/src/components/ViewChangeLog.tsx +208 -0
- package/src/components/ViewCheckResults.tsx +151 -0
- package/src/components/index.ts +12 -0
- package/src/config.ts +10 -0
- package/src/declare.d.ts +3 -0
- package/src/extensions/annotationFromPileup.ts +208 -0
- package/src/extensions/index.ts +1 -0
- package/src/index.ts +394 -0
- package/src/makeDisplayComponent.tsx +244 -0
- package/src/session/ClientDataStore.ts +282 -0
- package/src/session/index.ts +1 -0
- package/src/session/session.ts +373 -0
- package/src/types.ts +10 -0
- package/src/util/index.ts +31 -0
- package/src/util/loadAssemblyIntoClient.ts +291 -0
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import { ConfigurationReference, getConf } from '@jbrowse/core/configuration'
|
|
2
|
+
import { InternetAccount } from '@jbrowse/core/pluggableElementTypes'
|
|
3
|
+
import {
|
|
4
|
+
AbstractSessionModel,
|
|
5
|
+
isAbstractMenuManager,
|
|
6
|
+
isElectron,
|
|
7
|
+
} from '@jbrowse/core/util'
|
|
8
|
+
import { Change } from 'apollo-common'
|
|
9
|
+
import {
|
|
10
|
+
ChangeMessage,
|
|
11
|
+
CheckResultUpdate,
|
|
12
|
+
RequestUserInformationMessage,
|
|
13
|
+
UserLocation,
|
|
14
|
+
UserLocationMessage,
|
|
15
|
+
getDecodedToken,
|
|
16
|
+
makeUserSessionId,
|
|
17
|
+
} from 'apollo-shared'
|
|
18
|
+
import { autorun } from 'mobx'
|
|
19
|
+
import { Instance, flow, getRoot, types } from 'mobx-state-tree'
|
|
20
|
+
import { io } from 'socket.io-client'
|
|
21
|
+
|
|
22
|
+
import { ApolloSessionModel, Collaborator } from '../session'
|
|
23
|
+
import { ApolloRootModel } from '../types'
|
|
24
|
+
import { createFetchErrorMessage } from '../util'
|
|
25
|
+
import { addMenuItems } from './addMenuItems'
|
|
26
|
+
import { AuthTypeSelector } from './components/AuthTypeSelector'
|
|
27
|
+
import { ApolloInternetAccountConfigModel } from './configSchema'
|
|
28
|
+
|
|
29
|
+
type AuthType = 'google' | 'microsoft' | 'guest'
|
|
30
|
+
|
|
31
|
+
type Role = 'admin' | 'user' | 'readOnly'
|
|
32
|
+
|
|
33
|
+
const inWebWorker = typeof sessionStorage === 'undefined'
|
|
34
|
+
|
|
35
|
+
const stateModelFactory = (configSchema: ApolloInternetAccountConfigModel) => {
|
|
36
|
+
return InternetAccount.named('ApolloInternetAccount')
|
|
37
|
+
.props({
|
|
38
|
+
type: types.literal('ApolloInternetAccount'),
|
|
39
|
+
configuration: ConfigurationReference(configSchema),
|
|
40
|
+
})
|
|
41
|
+
.views((self) => ({
|
|
42
|
+
get baseURL(): string {
|
|
43
|
+
return getConf(self, 'baseURL')
|
|
44
|
+
},
|
|
45
|
+
getUserId() {
|
|
46
|
+
const token = self.retrieveToken()
|
|
47
|
+
if (!token) {
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
const dec = getDecodedToken(token)
|
|
51
|
+
return dec.id
|
|
52
|
+
},
|
|
53
|
+
}))
|
|
54
|
+
.volatile(() => ({
|
|
55
|
+
role: undefined as Role | undefined,
|
|
56
|
+
}))
|
|
57
|
+
.actions((self) => {
|
|
58
|
+
let roleNotificationSent = false
|
|
59
|
+
return {
|
|
60
|
+
setRole() {
|
|
61
|
+
const token = self.retrieveToken()
|
|
62
|
+
if (!token) {
|
|
63
|
+
self.role = undefined
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
const dec = getDecodedToken(token)
|
|
67
|
+
const { role } = dec
|
|
68
|
+
if (!role && !roleNotificationSent) {
|
|
69
|
+
const { session } = getRoot<ApolloRootModel>(self)
|
|
70
|
+
;(session as unknown as AbstractSessionModel).notify(
|
|
71
|
+
'You have registered as a user but have not been given access. Ask your administrator to enable access for your account.',
|
|
72
|
+
'warning',
|
|
73
|
+
)
|
|
74
|
+
// notify
|
|
75
|
+
roleNotificationSent = true
|
|
76
|
+
}
|
|
77
|
+
if (self.role !== role) {
|
|
78
|
+
self.role = role
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
.actions((self) => {
|
|
84
|
+
let listener: (event: MessageEvent) => void
|
|
85
|
+
return {
|
|
86
|
+
addMessageChannel(
|
|
87
|
+
resolve: (token: string) => void,
|
|
88
|
+
reject: (error: Error) => void,
|
|
89
|
+
) {
|
|
90
|
+
listener = (event) => {
|
|
91
|
+
// this should probably get better handling, but ignored for now
|
|
92
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
93
|
+
this.finishOAuthWindow(event, resolve, reject)
|
|
94
|
+
}
|
|
95
|
+
window.addEventListener('message', listener)
|
|
96
|
+
},
|
|
97
|
+
deleteMessageChannel() {
|
|
98
|
+
window.removeEventListener('message', listener)
|
|
99
|
+
},
|
|
100
|
+
async finishOAuthWindow(
|
|
101
|
+
event: MessageEvent,
|
|
102
|
+
resolve: (token: string) => void,
|
|
103
|
+
reject: (error: Error) => void,
|
|
104
|
+
) {
|
|
105
|
+
if (
|
|
106
|
+
event.data.name !== `JBrowseAuthWindow-${self.internetAccountId}`
|
|
107
|
+
) {
|
|
108
|
+
return this.deleteMessageChannel()
|
|
109
|
+
}
|
|
110
|
+
const redirectUriWithInfo = event.data.redirectUri
|
|
111
|
+
const fixedQueryString = redirectUriWithInfo.replace('#', '?')
|
|
112
|
+
const redirectUrl = new URL(fixedQueryString)
|
|
113
|
+
const queryStringSearch = redirectUrl.search
|
|
114
|
+
const urlParams = new URLSearchParams(queryStringSearch)
|
|
115
|
+
const token = urlParams.get('access_token')
|
|
116
|
+
this.deleteMessageChannel()
|
|
117
|
+
if (!token) {
|
|
118
|
+
return reject(new Error('Error with token endpoint'))
|
|
119
|
+
}
|
|
120
|
+
self.storeToken(token)
|
|
121
|
+
self.setRole()
|
|
122
|
+
return resolve(token)
|
|
123
|
+
},
|
|
124
|
+
async openAuthWindow(
|
|
125
|
+
type: string,
|
|
126
|
+
resolve: (token: string) => void,
|
|
127
|
+
reject: (error: Error) => void,
|
|
128
|
+
) {
|
|
129
|
+
const redirectUri = isElectron
|
|
130
|
+
? 'http://localhost/auth'
|
|
131
|
+
: window.location.origin + window.location.pathname
|
|
132
|
+
const url = new URL('auth/login', self.baseURL)
|
|
133
|
+
const params = new URLSearchParams({
|
|
134
|
+
type,
|
|
135
|
+
redirect_uri: redirectUri,
|
|
136
|
+
})
|
|
137
|
+
url.search = params.toString()
|
|
138
|
+
const eventName = `JBrowseAuthWindow-${self.internetAccountId}`
|
|
139
|
+
if (isElectron) {
|
|
140
|
+
const { ipcRenderer } = window.require('electron')
|
|
141
|
+
const redirectUriFromElectron = await ipcRenderer.invoke(
|
|
142
|
+
'openAuthWindow',
|
|
143
|
+
{
|
|
144
|
+
internetAccountId: self.internetAccountId,
|
|
145
|
+
data: { redirect_uri: redirectUri },
|
|
146
|
+
url: url.toString(),
|
|
147
|
+
},
|
|
148
|
+
)
|
|
149
|
+
const eventFromDesktop = new MessageEvent('message', {
|
|
150
|
+
data: { name: eventName, redirectUri: redirectUriFromElectron },
|
|
151
|
+
})
|
|
152
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
153
|
+
this.finishOAuthWindow(eventFromDesktop, resolve, reject)
|
|
154
|
+
} else {
|
|
155
|
+
this.addMessageChannel(resolve, reject)
|
|
156
|
+
window.open(url, eventName, 'width=500,height=600')
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
.actions((self) => ({
|
|
162
|
+
async getTokenFromUser(
|
|
163
|
+
resolve: (token: string) => void,
|
|
164
|
+
reject: (error: Error) => void,
|
|
165
|
+
): Promise<void> {
|
|
166
|
+
const { baseURL } = self
|
|
167
|
+
const authType = await new Promise(
|
|
168
|
+
(resolve: (authType: AuthType) => void, reject) => {
|
|
169
|
+
const { session } = getRoot<ApolloRootModel>(self)
|
|
170
|
+
const { baseURL, name } = self
|
|
171
|
+
;(session as unknown as AbstractSessionModel).queueDialog(
|
|
172
|
+
(doneCallback: () => void) => [
|
|
173
|
+
AuthTypeSelector,
|
|
174
|
+
{
|
|
175
|
+
name,
|
|
176
|
+
handleClose: (newAuthType?: AuthType | Error) => {
|
|
177
|
+
if (!newAuthType) {
|
|
178
|
+
reject(new Error('user cancelled entry'))
|
|
179
|
+
} else if (newAuthType instanceof Error) {
|
|
180
|
+
reject(newAuthType)
|
|
181
|
+
} else {
|
|
182
|
+
resolve(newAuthType)
|
|
183
|
+
}
|
|
184
|
+
doneCallback()
|
|
185
|
+
},
|
|
186
|
+
baseURL,
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
)
|
|
190
|
+
},
|
|
191
|
+
)
|
|
192
|
+
if (authType !== 'guest') {
|
|
193
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
194
|
+
self.openAuthWindow(authType, resolve, reject)
|
|
195
|
+
return
|
|
196
|
+
}
|
|
197
|
+
const url = new URL('auth/login', baseURL)
|
|
198
|
+
const searchParams = new URLSearchParams({ type: authType })
|
|
199
|
+
url.search = searchParams.toString()
|
|
200
|
+
const uri = url.toString()
|
|
201
|
+
const response = await fetch(uri)
|
|
202
|
+
if (!response.ok) {
|
|
203
|
+
const errorMessage = await createFetchErrorMessage(
|
|
204
|
+
response,
|
|
205
|
+
'Error when logging in',
|
|
206
|
+
)
|
|
207
|
+
return reject(new Error(errorMessage))
|
|
208
|
+
}
|
|
209
|
+
const { token } = await response.json()
|
|
210
|
+
resolve(token)
|
|
211
|
+
},
|
|
212
|
+
}))
|
|
213
|
+
.volatile(() => ({
|
|
214
|
+
lastChangeSequenceNumber: undefined as number | undefined,
|
|
215
|
+
}))
|
|
216
|
+
.actions((self) => ({
|
|
217
|
+
setLastChangeSequenceNumber(sequenceNumber: number) {
|
|
218
|
+
self.lastChangeSequenceNumber = sequenceNumber
|
|
219
|
+
},
|
|
220
|
+
}))
|
|
221
|
+
.actions((self) => ({
|
|
222
|
+
updateLastChangeSequenceNumber: flow(
|
|
223
|
+
function* updateLastChangeSequenceNumber() {
|
|
224
|
+
const { baseURL } = self
|
|
225
|
+
const url = new URL('changes', baseURL)
|
|
226
|
+
const searchParams = new URLSearchParams({ limit: '1' })
|
|
227
|
+
url.search = searchParams.toString()
|
|
228
|
+
const uri = url.toString()
|
|
229
|
+
const apolloFetch = self.getFetcher({
|
|
230
|
+
locationType: 'UriLocation',
|
|
231
|
+
uri,
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
const response = yield apolloFetch(uri, { method: 'GET' })
|
|
235
|
+
if (!response.ok) {
|
|
236
|
+
const errorMessage = yield createFetchErrorMessage(
|
|
237
|
+
response,
|
|
238
|
+
'Error when fetching server LastChangeSequence',
|
|
239
|
+
)
|
|
240
|
+
throw new Error(errorMessage)
|
|
241
|
+
}
|
|
242
|
+
const changes = yield response.json()
|
|
243
|
+
const sequence = changes.length > 0 ? changes[0].sequence : 0
|
|
244
|
+
self.setLastChangeSequenceNumber(sequence)
|
|
245
|
+
},
|
|
246
|
+
),
|
|
247
|
+
getMissingChanges: flow(function* getMissingChanges() {
|
|
248
|
+
const { session } = getRoot<ApolloRootModel>(self)
|
|
249
|
+
const { changeManager } = (session as ApolloSessionModel)
|
|
250
|
+
.apolloDataStore
|
|
251
|
+
if (!self.lastChangeSequenceNumber) {
|
|
252
|
+
throw new Error(
|
|
253
|
+
'No LastChangeSequence stored in session. Please, refresh you browser to get last updates from server',
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
const { baseURL, lastChangeSequenceNumber } = self
|
|
257
|
+
|
|
258
|
+
const url = new URL('changes', baseURL)
|
|
259
|
+
const searchParams = new URLSearchParams({
|
|
260
|
+
since: String(lastChangeSequenceNumber),
|
|
261
|
+
sort: '1',
|
|
262
|
+
})
|
|
263
|
+
url.search = searchParams.toString()
|
|
264
|
+
const uri = url.toString()
|
|
265
|
+
const apolloFetch = self.getFetcher({
|
|
266
|
+
locationType: 'UriLocation',
|
|
267
|
+
uri,
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
const response = yield apolloFetch(uri, { method: 'GET' })
|
|
271
|
+
if (!response.ok) {
|
|
272
|
+
console.error(
|
|
273
|
+
`Error when fetching the last updates to recover socket connection — ${response.status}`,
|
|
274
|
+
)
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
const serializedChanges = yield response.json()
|
|
278
|
+
for (const serializedChange of serializedChanges) {
|
|
279
|
+
const change = Change.fromJSON(serializedChange)
|
|
280
|
+
void changeManager?.submit(change, { submitToBackend: false })
|
|
281
|
+
}
|
|
282
|
+
}),
|
|
283
|
+
}))
|
|
284
|
+
.volatile((self) => ({
|
|
285
|
+
socket: io(self.baseURL),
|
|
286
|
+
}))
|
|
287
|
+
.actions((self) => ({
|
|
288
|
+
addSocketListeners() {
|
|
289
|
+
const { session } = getRoot<ApolloRootModel>(self)
|
|
290
|
+
const { notify } = session as unknown as AbstractSessionModel
|
|
291
|
+
const token = self.retrieveToken()
|
|
292
|
+
if (!token) {
|
|
293
|
+
throw new Error('No Token found')
|
|
294
|
+
}
|
|
295
|
+
const { socket } = self
|
|
296
|
+
const { addCheckResult, changeManager, deleteCheckResult } = (
|
|
297
|
+
session as ApolloSessionModel
|
|
298
|
+
).apolloDataStore
|
|
299
|
+
socket.on('connect', async () => {
|
|
300
|
+
await self.getMissingChanges()
|
|
301
|
+
})
|
|
302
|
+
socket.on('connect_error', () => {
|
|
303
|
+
notify('Could not connect to the Apollo server.', 'error')
|
|
304
|
+
})
|
|
305
|
+
socket.on('COMMON', (message: ChangeMessage | CheckResultUpdate) => {
|
|
306
|
+
if ('checkResult' in message) {
|
|
307
|
+
if (message.deleted) {
|
|
308
|
+
deleteCheckResult(message.checkResult._id.toString())
|
|
309
|
+
} else {
|
|
310
|
+
addCheckResult(message.checkResult)
|
|
311
|
+
}
|
|
312
|
+
return
|
|
313
|
+
}
|
|
314
|
+
// Save server last change sequence into session storage
|
|
315
|
+
sessionStorage.setItem(
|
|
316
|
+
'LastChangeSequence',
|
|
317
|
+
String(message.changeSequence),
|
|
318
|
+
)
|
|
319
|
+
if (message.userSessionId === token) {
|
|
320
|
+
return // we did this change, no need to apply it again
|
|
321
|
+
}
|
|
322
|
+
const change = Change.fromJSON(message.changeInfo)
|
|
323
|
+
void changeManager?.submit(change, { submitToBackend: false })
|
|
324
|
+
})
|
|
325
|
+
socket.on('USER_LOCATION', (message: UserLocationMessage) => {
|
|
326
|
+
const { channel, locations, userName, userSessionId } = message
|
|
327
|
+
const user = getDecodedToken(token)
|
|
328
|
+
const localSessionId = makeUserSessionId(user)
|
|
329
|
+
if (channel === 'USER_LOCATION' && userSessionId !== localSessionId) {
|
|
330
|
+
const collaborator: Collaborator = {
|
|
331
|
+
name: userName,
|
|
332
|
+
id: userSessionId,
|
|
333
|
+
locations,
|
|
334
|
+
}
|
|
335
|
+
session.addOrUpdateCollaborator(collaborator)
|
|
336
|
+
}
|
|
337
|
+
})
|
|
338
|
+
socket.on(
|
|
339
|
+
'REQUEST_INFORMATION',
|
|
340
|
+
(message: RequestUserInformationMessage) => {
|
|
341
|
+
const { channel, reqType, userSessionId } = message
|
|
342
|
+
if (channel === 'REQUEST_INFORMATION' && userSessionId !== token) {
|
|
343
|
+
switch (reqType) {
|
|
344
|
+
case 'CURRENT_LOCATION': {
|
|
345
|
+
session.broadcastLocations()
|
|
346
|
+
break
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
)
|
|
352
|
+
},
|
|
353
|
+
}))
|
|
354
|
+
.actions((self) => {
|
|
355
|
+
async function postUserLocation(userLoc: UserLocation[]) {
|
|
356
|
+
const { baseURL } = self
|
|
357
|
+
const url = new URL('users/userLocation', baseURL).href
|
|
358
|
+
const userLocation = new URLSearchParams(JSON.stringify(userLoc))
|
|
359
|
+
|
|
360
|
+
const apolloFetch = self.getFetcher({
|
|
361
|
+
locationType: 'UriLocation',
|
|
362
|
+
uri: url,
|
|
363
|
+
})
|
|
364
|
+
try {
|
|
365
|
+
const response = await apolloFetch(url, {
|
|
366
|
+
method: 'POST',
|
|
367
|
+
body: userLocation,
|
|
368
|
+
})
|
|
369
|
+
if (!response.ok) {
|
|
370
|
+
throw new Error('ignore') // ignore message, will get caught by "catch"
|
|
371
|
+
}
|
|
372
|
+
} catch {
|
|
373
|
+
console.error('Broadcasting user location failed')
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const debounceTimeout = 300
|
|
377
|
+
const debouncePostUserLocation = (
|
|
378
|
+
fn: (userLocation: UserLocation[]) => void,
|
|
379
|
+
) => {
|
|
380
|
+
let timeoutId: ReturnType<typeof setTimeout>
|
|
381
|
+
return (userLocation: UserLocation[]) => {
|
|
382
|
+
clearTimeout(timeoutId)
|
|
383
|
+
timeoutId = setTimeout(() => fn(userLocation), debounceTimeout)
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return { postUserLocation: debouncePostUserLocation(postUserLocation) }
|
|
387
|
+
})
|
|
388
|
+
.actions((self) => ({
|
|
389
|
+
initialize: flow(function* initialize(role: Role) {
|
|
390
|
+
if (role === 'admin') {
|
|
391
|
+
const rootModel = getRoot(self)
|
|
392
|
+
if (isAbstractMenuManager(rootModel)) {
|
|
393
|
+
addMenuItems(rootModel)
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
// Get and set server last change sequence into session storage
|
|
397
|
+
yield self.updateLastChangeSequenceNumber()
|
|
398
|
+
// Open socket listeners
|
|
399
|
+
self.addSocketListeners()
|
|
400
|
+
// request user locations
|
|
401
|
+
const { baseURL } = self
|
|
402
|
+
const uri = new URL('/users/locations', baseURL).href
|
|
403
|
+
const apolloFetch = self.getFetcher({
|
|
404
|
+
locationType: 'UriLocation',
|
|
405
|
+
uri,
|
|
406
|
+
})
|
|
407
|
+
yield apolloFetch(uri, { method: 'GET' })
|
|
408
|
+
window.addEventListener('beforeunload', () => {
|
|
409
|
+
self.postUserLocation([])
|
|
410
|
+
})
|
|
411
|
+
document.addEventListener('visibilitychange', () => {
|
|
412
|
+
// fires when user switches tabs, apps, goes to homescreen, etc.
|
|
413
|
+
if (document.visibilityState === 'hidden') {
|
|
414
|
+
self.postUserLocation([])
|
|
415
|
+
}
|
|
416
|
+
// fires when app transitions from prerender, user returns to the app / tab.
|
|
417
|
+
if (document.visibilityState === 'visible') {
|
|
418
|
+
const { session } = getRoot<ApolloRootModel>(self)
|
|
419
|
+
session.broadcastLocations()
|
|
420
|
+
}
|
|
421
|
+
})
|
|
422
|
+
}),
|
|
423
|
+
}))
|
|
424
|
+
.actions((self) => ({
|
|
425
|
+
afterAttach() {
|
|
426
|
+
self.setRole()
|
|
427
|
+
autorun(
|
|
428
|
+
async (reaction) => {
|
|
429
|
+
if (inWebWorker) {
|
|
430
|
+
return
|
|
431
|
+
}
|
|
432
|
+
if (self.role) {
|
|
433
|
+
await self.initialize(self.role)
|
|
434
|
+
reaction.dispose()
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
{ name: 'ApolloInternetAccount' },
|
|
438
|
+
)
|
|
439
|
+
},
|
|
440
|
+
}))
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export default stateModelFactory
|
|
444
|
+
export type ApolloInternetAccountStateModel = ReturnType<
|
|
445
|
+
typeof stateModelFactory
|
|
446
|
+
>
|
|
447
|
+
export type ApolloInternetAccountModel =
|
|
448
|
+
Instance<ApolloInternetAccountStateModel>
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SessionWithWidgets,
|
|
3
|
+
getSession,
|
|
4
|
+
isSessionModelWithWidgets,
|
|
5
|
+
} from '@jbrowse/core/util'
|
|
6
|
+
import { types } from 'mobx-state-tree'
|
|
7
|
+
|
|
8
|
+
interface JobsEntry {
|
|
9
|
+
name: string
|
|
10
|
+
cancelCallback?: () => void
|
|
11
|
+
progressPct?: number
|
|
12
|
+
statusMessage?: string
|
|
13
|
+
}
|
|
14
|
+
interface JobsListModel {
|
|
15
|
+
id: number
|
|
16
|
+
type: 'JobsListWidget'
|
|
17
|
+
jobs: JobsEntry[]
|
|
18
|
+
finished: JobsEntry[]
|
|
19
|
+
queued: JobsEntry[]
|
|
20
|
+
aborted: JobsEntry[]
|
|
21
|
+
addJob(job: JobsEntry): void
|
|
22
|
+
removeJob(jobName: string): void
|
|
23
|
+
addFinishedJob(job: JobsEntry): void
|
|
24
|
+
addQueuedJob(job: JobsEntry): void
|
|
25
|
+
addAbortedJob(job: JobsEntry): void
|
|
26
|
+
removeQueuedJob(jobName: string): void
|
|
27
|
+
updateJobStatusMessage(jobName: string, message?: string): void
|
|
28
|
+
updateJobProgressPct(jobName: string, pct: number): void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const ApolloJobModel = types
|
|
32
|
+
.model('JobsManager', {})
|
|
33
|
+
.views((self) => ({
|
|
34
|
+
get jobStatusWidget() {
|
|
35
|
+
const { widgets } = getSession(self) as SessionWithWidgets
|
|
36
|
+
let jobStatusWidget = widgets.get('JobsList')
|
|
37
|
+
if (!jobStatusWidget) {
|
|
38
|
+
// @ts-expect-error: addWidget function not detected on the session
|
|
39
|
+
jobStatusWidget = getSession(self).addWidget(
|
|
40
|
+
'JobsListWidget',
|
|
41
|
+
'JobsList',
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
return jobStatusWidget as unknown as JobsListModel
|
|
45
|
+
},
|
|
46
|
+
}))
|
|
47
|
+
.actions((self) => ({
|
|
48
|
+
/**
|
|
49
|
+
* updates the status message and the progress percent of the provided job
|
|
50
|
+
* @param jobName - the name of the job to be updated
|
|
51
|
+
* @param statusMessage - the message to be communicated to the user
|
|
52
|
+
* @param progressPct - the percent through the run the job is
|
|
53
|
+
*/
|
|
54
|
+
update(jobName: string, statusMessage: string, progressPct?: number) {
|
|
55
|
+
self.jobStatusWidget.updateJobStatusMessage(jobName, statusMessage)
|
|
56
|
+
if (progressPct) {
|
|
57
|
+
self.jobStatusWidget.updateJobProgressPct(jobName, progressPct)
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
/**
|
|
61
|
+
* aborts the provided job with a message to the user
|
|
62
|
+
* @param jobName - the name of the job to be aborted
|
|
63
|
+
* @param msg - a message to communicate to the user about the abort operation
|
|
64
|
+
*/
|
|
65
|
+
abortJob(jobName: string, msg?: string) {
|
|
66
|
+
const session = getSession(self)
|
|
67
|
+
if (isSessionModelWithWidgets(session)) {
|
|
68
|
+
session.showWidget(self.jobStatusWidget)
|
|
69
|
+
self.jobStatusWidget.updateJobStatusMessage(
|
|
70
|
+
jobName,
|
|
71
|
+
msg ?? 'Aborted unexpectedly',
|
|
72
|
+
)
|
|
73
|
+
// this is done to avoid issues with reusing nodes from other state trees
|
|
74
|
+
const indx = self.jobStatusWidget.jobs.findIndex(
|
|
75
|
+
(job) => job.name === jobName,
|
|
76
|
+
)
|
|
77
|
+
const job = self.jobStatusWidget.jobs[indx]
|
|
78
|
+
// object needs to be shallow copied before it is removed from the state tree
|
|
79
|
+
self.jobStatusWidget.addAbortedJob({ ...job })
|
|
80
|
+
// removes the job from the state tree, this node is inaccessible thereafter
|
|
81
|
+
self.jobStatusWidget.removeJob(jobName) as unknown as JobsEntry
|
|
82
|
+
session.notify('Job aborted', 'info')
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
/**
|
|
86
|
+
* opens the job status widget and adds the job to the running jobs
|
|
87
|
+
* @param job - the job to be run within the JobsManager
|
|
88
|
+
*/
|
|
89
|
+
runJob(job: JobsEntry) {
|
|
90
|
+
const session = getSession(self)
|
|
91
|
+
if (isSessionModelWithWidgets(session)) {
|
|
92
|
+
session.showWidget(self.jobStatusWidget)
|
|
93
|
+
self.jobStatusWidget.addJob(job)
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
/**
|
|
97
|
+
* sets the progress and status message of the provided job
|
|
98
|
+
* adds the finished jobs to the list of finished jobs
|
|
99
|
+
* clears the jobs manager of the now done job
|
|
100
|
+
* begins to run the next job if one is queued
|
|
101
|
+
* @param job - the job to be completed
|
|
102
|
+
*/
|
|
103
|
+
done(job: JobsEntry) {
|
|
104
|
+
const session = getSession(self)
|
|
105
|
+
if (isSessionModelWithWidgets(session)) {
|
|
106
|
+
session.showWidget(self.jobStatusWidget)
|
|
107
|
+
// this.setProgressPct(100)
|
|
108
|
+
self.jobStatusWidget.removeJob(job.name)
|
|
109
|
+
self.jobStatusWidget.addFinishedJob({
|
|
110
|
+
name: job.name,
|
|
111
|
+
statusMessage: 'All operations successful',
|
|
112
|
+
progressPct: 100,
|
|
113
|
+
cancelCallback: job.cancelCallback,
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
}))
|