link-sharing.jsx 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. import { useCallback, useState, useEffect } from 'react'
  2. import PropTypes from 'prop-types'
  3. import { useTranslation } from 'react-i18next'
  4. import Icon from '@/shared/components/icon'
  5. import { useShareProjectContext } from './share-project-modal'
  6. import { setProjectAccessLevel } from '../utils/api'
  7. import { CopyToClipboard } from '@/shared/components/copy-to-clipboard'
  8. import { useProjectContext } from '@/shared/context/project-context'
  9. import * as eventTracking from '../../../infrastructure/event-tracking'
  10. import { useUserContext } from '@/shared/context/user-context'
  11. import { sendMB } from '../../../infrastructure/event-tracking'
  12. import { getJSON } from '../../../infrastructure/fetch-json'
  13. import useAbortController from '@/shared/hooks/use-abort-controller'
  14. import { debugConsole } from '@/utils/debugging'
  15. import getMeta from '@/utils/meta'
  16. import OLRow from '@/features/ui/components/ol/ol-row'
  17. import OLCol from '@/features/ui/components/ol/ol-col'
  18. import OLButton from '@/features/ui/components/ol/ol-button'
  19. import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
  20. import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
  21. import MaterialIcon from '@/shared/components/material-icon'
  22. export default function LinkSharing() {
  23. const [inflight, setInflight] = useState(false)
  24. const [showLinks, setShowLinks] = useState(true)
  25. const { monitorRequest } = useShareProjectContext()
  26. const { _id: projectId, publicAccessLevel } = useProjectContext()
  27. // set the access level of a project
  28. const setAccessLevel = useCallback(
  29. newPublicAccessLevel => {
  30. setInflight(true)
  31. sendMB('link-sharing-click-off', {
  32. project_id: projectId,
  33. })
  34. monitorRequest(() =>
  35. setProjectAccessLevel(projectId, newPublicAccessLevel)
  36. )
  37. .then(() => {
  38. // NOTE: not calling `updateProject` here as it receives data via
  39. // project:publicAccessLevel:changed over the websocket connection
  40. // TODO: eventTracking.sendMB('project-make-token-based') when newPublicAccessLevel is 'tokenBased'
  41. })
  42. .finally(() => {
  43. setInflight(false)
  44. })
  45. },
  46. [monitorRequest, projectId]
  47. )
  48. switch (publicAccessLevel) {
  49. // Private (with token-access available)
  50. case 'private':
  51. return (
  52. <PrivateSharing
  53. setAccessLevel={setAccessLevel}
  54. inflight={inflight}
  55. projectId={projectId}
  56. setShowLinks={setShowLinks}
  57. />
  58. )
  59. // Token-based access
  60. case 'tokenBased':
  61. return (
  62. <TokenBasedSharing
  63. setAccessLevel={setAccessLevel}
  64. inflight={inflight}
  65. setShowLinks={setShowLinks}
  66. showLinks={showLinks}
  67. />
  68. )
  69. // Legacy public-access
  70. case 'readAndWrite':
  71. case 'readOnly':
  72. return (
  73. <LegacySharing
  74. setAccessLevel={setAccessLevel}
  75. accessLevel={publicAccessLevel}
  76. inflight={inflight}
  77. />
  78. )
  79. default:
  80. return null
  81. }
  82. }
  83. function PrivateSharing({ setAccessLevel, inflight, projectId, setShowLinks }) {
  84. const { t } = useTranslation()
  85. return (
  86. <OLRow className="public-access-level">
  87. <OLCol xs={12} className="text-center">
  88. <strong>{t('link_sharing_is_off_short')}</strong>
  89. <span>&nbsp;&nbsp;</span>
  90. <OLButton
  91. variant="link"
  92. className="btn-inline-link"
  93. onClick={() => {
  94. setAccessLevel('tokenBased')
  95. eventTracking.sendMB('link-sharing-click', { projectId })
  96. setShowLinks(true)
  97. }}
  98. disabled={inflight}
  99. >
  100. {t('turn_on_link_sharing')}
  101. </OLButton>
  102. <span>&nbsp;&nbsp;</span>
  103. <LinkSharingInfo />
  104. </OLCol>
  105. </OLRow>
  106. )
  107. }
  108. PrivateSharing.propTypes = {
  109. setAccessLevel: PropTypes.func.isRequired,
  110. inflight: PropTypes.bool,
  111. projectId: PropTypes.string,
  112. setShowLinks: PropTypes.func.isRequired,
  113. }
  114. function TokenBasedSharing({
  115. setAccessLevel,
  116. inflight,
  117. setShowLinks,
  118. showLinks,
  119. }) {
  120. const { t } = useTranslation()
  121. const { _id: projectId } = useProjectContext()
  122. const [tokens, setTokens] = useState(null)
  123. const { signal } = useAbortController()
  124. useEffect(() => {
  125. getJSON(`/project/${projectId}/tokens`, { signal })
  126. .then(data => setTokens(data))
  127. .catch(debugConsole.error)
  128. }, [projectId, signal])
  129. return (
  130. <OLRow className="public-access-level">
  131. <OLCol xs={12} className="text-center">
  132. <strong>{t('link_sharing_is_on')}</strong>
  133. <span>&nbsp;&nbsp;</span>
  134. <OLButton
  135. variant="link"
  136. className="btn-inline-link"
  137. onClick={() => setAccessLevel('private')}
  138. disabled={inflight}
  139. >
  140. {t('turn_off_link_sharing')}
  141. </OLButton>
  142. <span>&nbsp;&nbsp;</span>
  143. <LinkSharingInfo />
  144. <OLButton
  145. variant="link"
  146. className="btn-chevron align-middle"
  147. onClick={() => setShowLinks(!showLinks)}
  148. >
  149. <BootstrapVersionSwitcher
  150. bs3={<Icon type={showLinks ? 'chevron-up' : 'chevron-down'} fw />}
  151. bs5={
  152. <MaterialIcon
  153. type={showLinks ? 'keyboard_arrow_up' : 'keyboard_arrow_down'}
  154. />
  155. }
  156. />
  157. </OLButton>
  158. </OLCol>
  159. {showLinks && (
  160. <OLCol xs={12} className="access-token-display-area">
  161. <div className="access-token-wrapper">
  162. <strong>{t('anyone_with_link_can_edit')}</strong>
  163. <AccessToken
  164. token={tokens?.readAndWrite}
  165. tokenHashPrefix={tokens?.readAndWriteHashPrefix}
  166. path="/"
  167. tooltipId="tooltip-copy-link-rw"
  168. />
  169. </div>
  170. <div className="access-token-wrapper">
  171. <strong>{t('anyone_with_link_can_view')}</strong>
  172. <AccessToken
  173. token={tokens?.readOnly}
  174. tokenHashPrefix={tokens?.readOnlyHashPrefix}
  175. path="/read/"
  176. tooltipId="tooltip-copy-link-ro"
  177. />
  178. </div>
  179. </OLCol>
  180. )}
  181. </OLRow>
  182. )
  183. }
  184. TokenBasedSharing.propTypes = {
  185. setAccessLevel: PropTypes.func.isRequired,
  186. inflight: PropTypes.bool,
  187. setShowLinks: PropTypes.func.isRequired,
  188. showLinks: PropTypes.bool,
  189. }
  190. function LegacySharing({ accessLevel, setAccessLevel, inflight }) {
  191. const { t } = useTranslation()
  192. return (
  193. <OLRow className="public-access-level">
  194. <OLCol xs={12} className="text-center">
  195. <strong>
  196. {accessLevel === 'readAndWrite' && t('this_project_is_public')}
  197. {accessLevel === 'readOnly' && t('this_project_is_public_read_only')}
  198. </strong>
  199. <span>&nbsp;&nbsp;</span>
  200. <OLButton
  201. variant="link"
  202. className="btn-inline-link"
  203. onClick={() => setAccessLevel('private')}
  204. disabled={inflight}
  205. >
  206. {t('make_private')}
  207. </OLButton>
  208. <span>&nbsp;&nbsp;</span>
  209. <LinkSharingInfo />
  210. </OLCol>
  211. </OLRow>
  212. )
  213. }
  214. LegacySharing.propTypes = {
  215. accessLevel: PropTypes.string.isRequired,
  216. setAccessLevel: PropTypes.func.isRequired,
  217. inflight: PropTypes.bool,
  218. }
  219. export function ReadOnlyTokenLink() {
  220. const { t } = useTranslation()
  221. const { _id: projectId } = useProjectContext()
  222. const [tokens, setTokens] = useState(null)
  223. const { signal } = useAbortController()
  224. useEffect(() => {
  225. getJSON(`/project/${projectId}/tokens`, { signal })
  226. .then(data => setTokens(data))
  227. .catch(debugConsole.error)
  228. }, [projectId, signal])
  229. return (
  230. <OLRow className="public-access-level">
  231. <OLCol className="access-token-display-area">
  232. <div className="access-token-wrapper">
  233. <strong>{t('anyone_with_link_can_view')}</strong>
  234. <AccessToken
  235. token={tokens?.readOnly}
  236. tokenHashPrefix={tokens?.readOnlyHashPrefix}
  237. path="/read/"
  238. tooltipId="tooltip-copy-link-ro"
  239. />
  240. </div>
  241. </OLCol>
  242. </OLRow>
  243. )
  244. }
  245. function AccessToken({ token, tokenHashPrefix, path, tooltipId }) {
  246. const { t } = useTranslation()
  247. const { isAdmin } = useUserContext()
  248. if (!token) {
  249. return (
  250. <pre className="access-token">
  251. <span>{t('loading')}…</span>
  252. </pre>
  253. )
  254. }
  255. let origin = window.location.origin
  256. if (isAdmin) {
  257. origin = getMeta('ol-ExposedSettings').siteUrl
  258. }
  259. const link = `${origin}${path}${token}${
  260. tokenHashPrefix ? `#${tokenHashPrefix}` : ''
  261. }`
  262. return (
  263. <div className="access-token">
  264. <code>{link}</code>
  265. <CopyToClipboard content={link} tooltipId={tooltipId} />
  266. </div>
  267. )
  268. }
  269. AccessToken.propTypes = {
  270. token: PropTypes.string,
  271. tokenHashPrefix: PropTypes.string,
  272. tooltipId: PropTypes.string.isRequired,
  273. path: PropTypes.string.isRequired,
  274. }
  275. function LinkSharingInfo() {
  276. const { t } = useTranslation()
  277. return (
  278. <OLTooltip
  279. id="link-sharing-info"
  280. description={t('learn_more_about_link_sharing')}
  281. >
  282. <a
  283. href="/learn/how-to/What_is_Link_Sharing%3F"
  284. target="_blank"
  285. rel="noopener"
  286. >
  287. <BootstrapVersionSwitcher
  288. bs3={<Icon type="question-circle" />}
  289. bs5={<MaterialIcon type="help" className="align-middle" />}
  290. />
  291. </a>
  292. </OLTooltip>
  293. )
  294. }