Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel
- Added a message note to the login page
- Download with terms of use and guestbook.
- Show terms modal before download when dataset has custom terms, a non-default license (not CC0 1.0), or a guestbook. Draft datasets and dataset editors bypass the modal.
- Layout: added a configurable homepage banner for announcements and important messages. (#787)

### Changed

Expand All @@ -33,6 +34,7 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel
- Added disclaimer text and custom popup text to Publish Dataset modal for better communication of the implications of publishing a dataset. The text can be configured through the runtime configuration.
- Dataset page Terms tab title now depends on permissions: users with dataset update permission see `Terms and Guestbook`, and read-only users see `Terms`.
- Avoided prop-drilling for collection repository, so used context to share epository instances.
- Added frontend version to the footer of the application, which is retrieved from the `version` field in `package.json` at build time.

### Fixed

Expand Down
147 changes: 147 additions & 0 deletions build/spaVersionMetadata.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { existsSync, readFileSync } from 'node:fs'
import * as path from 'node:path'

export function getNodeEnv() {
return globalThis.process?.env
}

function normalizeTag(tag) {
if (!tag) {
return undefined
}

return tag.replace(/^refs\/tags\//, '').replace(/^v/, '')
}

function resolveSpaVersionDisplay({ packageVersion, commitSha, exactTag, refName, refType }) {
const releaseTag = refType === 'tag' ? refName : exactTag

if (packageVersion && normalizeTag(releaseTag) === packageVersion) {
return packageVersion
}

if (commitSha) {
return commitSha
}

return packageVersion ?? 'unknown'
}

function readTextFile(filePath) {
try {
return readFileSync(filePath, 'utf8').trim() || undefined
} catch {
return undefined
}
}

export function resolveProjectRoot(configDir) {
const candidates = [configDir, path.resolve(configDir, '..'), path.resolve(configDir, '../..')]

const projectRoot = candidates.find((candidate) => {
return (
existsSync(path.join(candidate, 'package.json')) && existsSync(path.join(candidate, '.git'))
)
})

return projectRoot ?? configDir
}

function readPackageVersion(projectRoot) {
try {
const packageJson = JSON.parse(readFileSync(path.join(projectRoot, 'package.json'), 'utf8'))

return packageJson.version
} catch {
return undefined
}
}

function resolveGitDir(projectRoot) {
const dotGitPath = path.join(projectRoot, '.git')

if (!existsSync(dotGitPath)) {
return undefined
}

const dotGitContent = readTextFile(dotGitPath)

if (dotGitContent?.startsWith('gitdir: ')) {
return path.resolve(projectRoot, dotGitContent.slice('gitdir: '.length))
}

return dotGitPath
}

function readPackedRef(gitDir, ref) {
const packedRefs = readTextFile(path.join(gitDir, 'packed-refs'))

if (!packedRefs) {
return undefined
}

for (const line of packedRefs.split('\n')) {
if (!line || line.startsWith('#') || line.startsWith('^')) {
continue
}

const [sha, packedRef] = line.split(' ')
if (packedRef === ref) {
return sha
}
}

return undefined
}

function readGitHeadInfo(projectRoot) {
const gitDir = resolveGitDir(projectRoot)

if (!gitDir) {
return {}
}

const head = readTextFile(path.join(gitDir, 'HEAD'))

if (!head) {
return {}
}

if (!head.startsWith('ref: ')) {
return { commitSha: head.slice(0, 9) }
}

const ref = head.slice('ref: '.length)
const commitSha = readTextFile(path.join(gitDir, ref)) ?? readPackedRef(gitDir, ref)
const exactTag = ref.startsWith('refs/tags/') ? ref.replace('refs/tags/', '') : undefined

return {
commitSha: commitSha?.slice(0, 9),
exactTag
}
}

export function createSpaVersionDefines(projectRoot, nodeEnv = getNodeEnv()) {
const packageVersion = readPackageVersion(projectRoot)
const gitHeadInfo = readGitHeadInfo(projectRoot)
const shortCommitSha = nodeEnv?.GITHUB_SHA?.slice(0, 9) ?? gitHeadInfo.commitSha
const exactTag =
(nodeEnv?.GITHUB_REF_TYPE === 'tag' ? nodeEnv.GITHUB_REF_NAME : undefined) ??
gitHeadInfo.exactTag
const spaDisplayVersion = resolveSpaVersionDisplay({
packageVersion,
commitSha: shortCommitSha,
exactTag,
refName: nodeEnv?.GITHUB_REF_NAME,
refType: nodeEnv?.GITHUB_REF_TYPE
})

return {
'import.meta.env.VITE_APP_VERSION': JSON.stringify(packageVersion),
'import.meta.env.VITE_COMMIT_SHA_SHORT': JSON.stringify(shortCommitSha),
'import.meta.env.VITE_GIT_EXACT_TAG': JSON.stringify(exactTag),
'import.meta.env.VITE_GITHUB_REF_NAME': JSON.stringify(nodeEnv?.GITHUB_REF_NAME),
'import.meta.env.VITE_GITHUB_REF_TYPE': JSON.stringify(nodeEnv?.GITHUB_REF_TYPE),
'import.meta.env.VITE_SPA_DISPLAY_VERSION': JSON.stringify(spaDisplayVersion)
}
}
25 changes: 23 additions & 2 deletions dev-env/shib-dev-env/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,30 @@
import { existsSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import istanbul from 'vite-plugin-istanbul'
import * as path from 'path'

const sharedBuildModuleUrl = [
new URL('./build/spaVersionMetadata.mjs', import.meta.url),
new URL('../build/spaVersionMetadata.mjs', import.meta.url),
new URL('../../build/spaVersionMetadata.mjs', import.meta.url)
].find((candidate) => existsSync(fileURLToPath(candidate)))

if (!sharedBuildModuleUrl) {
throw new Error('Could not locate build/spaVersionMetadata.mjs')
}

const { createSpaVersionDefines, resolveProjectRoot } = (await import(sharedBuildModuleUrl.href)) as {
createSpaVersionDefines: (projectRoot: string) => Record<string, string>
resolveProjectRoot: (configDir: string) => string
}

const projectRoot = resolveProjectRoot(__dirname)

export default defineConfig({
root: projectRoot,
define: createSpaVersionDefines(projectRoot),
plugins: [
react(),
istanbul({
Expand All @@ -24,8 +45,8 @@ export default defineConfig({
},
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
'@tests': path.resolve(__dirname, 'tests')
'@': path.resolve(projectRoot, 'src'),
'@tests': path.resolve(projectRoot, 'tests')
}
}
})
25 changes: 23 additions & 2 deletions dev-env/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,31 @@
import { existsSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import istanbul from 'vite-plugin-istanbul'
import * as path from 'path'

const sharedBuildModuleUrl = [
new URL('./build/spaVersionMetadata.mjs', import.meta.url),
new URL('../build/spaVersionMetadata.mjs', import.meta.url),
new URL('../../build/spaVersionMetadata.mjs', import.meta.url)
].find((candidate) => existsSync(fileURLToPath(candidate)))

if (!sharedBuildModuleUrl) {
throw new Error('Could not locate build/spaVersionMetadata.mjs')
}

const { createSpaVersionDefines, resolveProjectRoot } = (await import(sharedBuildModuleUrl.href)) as {
createSpaVersionDefines: (projectRoot: string) => Record<string, string>
resolveProjectRoot: (configDir: string) => string
}

const projectRoot = resolveProjectRoot(__dirname)

export default defineConfig({
root: projectRoot,
base: '/modern',
define: createSpaVersionDefines(projectRoot),
plugins: [
react(),
istanbul({
Expand All @@ -26,8 +47,8 @@ export default defineConfig({
},
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
'@tests': path.resolve(__dirname, 'tests')
'@': path.resolve(projectRoot, 'src'),
'@tests': path.resolve(projectRoot, 'tests')
}
}
})
3 changes: 3 additions & 0 deletions public/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
window.__APP_CONFIG__ = {
// Base URL of your Dataverse backend
backendUrl: 'http://localhost:8000',
// Optional banner shown at the top of the app when set. Basic HTML markup is supported.
bannerMessage:
'You are using the new Dataverse <strong>Modern version</strong>. This is an early release and some features from the original site are not yet available.',
// OIDC provider settings
oidc: {
clientId: 'test',
Expand Down
3 changes: 2 additions & 1 deletion public/locales/en/footer.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"copyright": "Copyright © {{year}}, {{copyrightHolder}}",
"privacyPolicy": "Privacy Policy",
"poweredBy": "Powered by"
"poweredBy": "Powered by",
"spaVersion": "frontend version: {{version}}"
}
3 changes: 2 additions & 1 deletion public/locales/es/footer.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"copyright": "Copyright © {{year}}, {{copyrightHolder}}",
"privacyPolicy": "Política de privacidad",
"poweredBy": "Desarrollado por"
"poweredBy": "Desarrollado por",
"spaVersion": "versión del frontend: {{version}}"
}
1 change: 1 addition & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ let CONFIG: AppConfig | undefined

const AppConfigSchema = z.object({
backendUrl: z.url(),
bannerMessage: z.string().optional(),
oidc: z.object({
clientId: z.string(),
authorizationEndpoint: z.url(),
Expand Down
16 changes: 11 additions & 5 deletions src/sections/layout/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
import DOMPurify from 'dompurify'
import { Outlet } from 'react-router-dom'
import { Container } from '@iqss/dataverse-design-system'
import styles from './Layout.module.scss'
import { FooterFactory } from './footer/FooterFactory'
import TopBarProgressIndicator from './topbar-progress-indicator/TopbarProgressIndicator'
import { HeaderFactory } from './header/HeaderFactory'
import { HistoryTrackerProvider } from '@/router/HistoryTrackerProvider'
import { requireAppConfig } from '@/config'

export function Layout() {
const { bannerMessage } = requireAppConfig()
const sanitizedBannerMessage = bannerMessage
? DOMPurify.sanitize(bannerMessage, { USE_PROFILES: { html: true } })
: null

return (
<HistoryTrackerProvider>
<TopBarProgressIndicator />
{HeaderFactory.create()}
{/* <div className="alert alert-warning rounded-0" role="alert">
<div className="container">
You are using the new Dataverse <strong>Modern version</strong>. This is an early release and
some features from the original site are not yet available.
{sanitizedBannerMessage ? (
<div className="alert alert-warning rounded-0" role="alert">
<div className="container" dangerouslySetInnerHTML={{ __html: sanitizedBannerMessage }} />
</div>
</div> */}
) : null}

<main>
<Container className={styles['body-container']}>
Expand Down
17 changes: 16 additions & 1 deletion src/sections/layout/footer/Footer.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,30 @@
.powered-by-container {
font-size: $dv-font-size-sm;
text-align: right;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 0.25rem;
}

.branding-row,
.versions-row {
display: flex;
justify-content: flex-end;
align-items: center;
flex-wrap: wrap;
}

.powered-by-text {
margin-right: 0.6em;
}

.version {
margin-right: 0.3em;
white-space: nowrap;
text-align: right;
vertical-align: bottom;
}

.separator {
margin: 0 0.3em;
}
35 changes: 21 additions & 14 deletions src/sections/layout/footer/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useTranslation } from 'react-i18next'
import { Container, Row, Col } from '@iqss/dataverse-design-system'
import { requireAppConfig } from '@/config'
import { spaVersion } from '@/version/spaVersion'
import { DataverseInfoRepository } from '../../../info/domain/repositories/DataverseInfoRepository'
import { useDataverseVersion } from './useDataverseVersion'
import dataverseProjectLogo from '@/assets/dataverse-project-logo.svg'
Expand Down Expand Up @@ -41,20 +42,26 @@ export function Footer({ dataverseInfoRepository }: FooterProps) {
</Col>
<Col sm={4}>
<div className={styles['powered-by-container']}>
<span className={styles['powered-by-text']}>{t('poweredBy')}</span>
<a
href="https://dataverse.org/"
title="The Dataverse Project"
target="_blank"
rel="noreferrer">
<img
src={dataverseProjectLogo}
width="118"
height="40"
alt="The Dataverse Project logo"
/>
</a>
<span className={styles.version}>{dataverseVersion}</span>
<div className={styles['branding-row']}>
<span className={styles['powered-by-text']}>{t('poweredBy')}</span>
<a
href="https://dataverse.org/"
title="The Dataverse Project"
target="_blank"
rel="noreferrer">
<img
src={dataverseProjectLogo}
width="118"
height="40"
alt="The Dataverse Project logo"
/>
</a>
</div>
<div className={styles['versions-row']}>
{dataverseVersion && <span className={styles.version}>{dataverseVersion}</span>}
{dataverseVersion && <span className={styles.separator}>|</span>}
<span className={styles.version}>{t('spaVersion', { version: spaVersion })}</span>
</div>
</div>
</Col>
</Row>
Expand Down
Loading
Loading