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

Show Central version in modal #1099

Draft
wants to merge 7 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 4 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
43 changes: 5 additions & 38 deletions src/components/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,11 @@ import { START_LOCATION, useRouter, useRoute } from 'vue-router';
import Alert from './alert.vue';
import Navbar from './navbar.vue';

import useCallWait from '../composables/call-wait';
import useCentralVersion from '../composables/central-version';
import useDisabled from '../composables/disabled';
import useFeatureFlags from '../composables/feature-flags';
import { useRequestData } from '../request-data';
import { useSessions } from '../util/session';
import { loadAsync } from '../util/load-async';
import { useSessions } from '../util/session';

export default {
name: 'App',
Expand All @@ -61,7 +60,9 @@ export default {
inject: ['alert', 'config'],
setup() {
const { visiblyLoggedIn } = useSessions();
useCentralVersion();
useDisabled();
const { features } = useFeatureFlags();

const router = useRouter();
const route = useRoute();
Expand All @@ -71,11 +72,7 @@ export default {
document.documentElement.style.backgroundColor = 'var(--color-accent-secondary)';
});

const { features } = useFeatureFlags();

const { centralVersion } = useRequestData();
const { callWait } = useCallWait();
return { visiblyLoggedIn, centralVersion, callWait, features };
return { features, visiblyLoggedIn };
},
computed: {
routerReady() {
Expand All @@ -86,41 +83,11 @@ export default {
this.visiblyLoggedIn;
},
},
created() {
this.callWait('checkVersion', this.checkVersion, (tries) =>
(tries === 0 ? 15000 : 60000));
},
// Reset backgroundColor after each test.
beforeUnmount() {
document.documentElement.style.backgroundColor = '';
},
methods: {
checkVersion() {
const previousVersion = this.centralVersion.versionText;
return this.centralVersion.request({
url: '/version.txt',
clear: false,
alert: false
})
.then(() => {
if (previousVersion == null || this.centralVersion.versionText === previousVersion)
return false;

// Alert the user about the version change, then keep alerting them.
// One benefit of this approach is that the user should see the alert
// even if there is another alert (say, about session expiration).
this.callWait(
'alertVersionChange',
() => { this.alert.info(this.$t('alert.versionChange')); },
(count) => (count === 0 ? 0 : 60000)
);
return true;
})
// This error could be the result of logout, which will cancel all
// requests.
.catch(error =>
(error.response != null && error.response.status === 404));
},
hideAlertAfterClick(event) {
if (this.alert.state && event.target.closest('a[target="_blank"]') != null &&
!event.defaultPrevented) {
Expand Down
64 changes: 64 additions & 0 deletions src/components/central-version.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<!--
Copyright 2024 ODK Central Developers
See the NOTICE file at the top-level directory of this distribution and at
https://github.com/getodk/central-frontend/blob/master/NOTICE.

This file is part of ODK Central. It is subject to the license terms in
the LICENSE file found in the top-level directory of this distribution and at
https://www.apache.org/licenses/LICENSE-2.0. No part of ODK Central,
including this file, may be copied, modified, propagated, or distributed
except according to the terms contained in the LICENSE file.
-->
<template>
<modal id="central-version" :state="state" hideable size="large" backdrop
@hide="$emit('hide')">
<template #title>{{ $t('title') }}</template>
<template #body>
<div class="modal-introduction">
<p>{{ $t('shortVersion', { version: centralVersion.currentVersion }) }}</p>
<p>{{ $t('longVersion') }}</p>
<pre><code><selectable wrap>{{ centralVersion.versionText }}</selectable></code></pre>
</div>
<div class="modal-actions">
<button type="button" class="btn btn-primary" @click="$emit('hide')">
{{ $t('action.close') }}
</button>
</div>
</template>
</modal>
</template>

<script setup>
import Modal from './modal.vue';
import Selectable from './selectable.vue';

import { useRequestData } from '../request-data';

defineOptions({
name: 'CentralVersion'
});
defineProps({
state: Boolean
});
defineEmits(['hide']);

const { centralVersion } = useRequestData();
</script>

<style lang="scss">
#central-version {
.loading { min-height: 120px; }
}
</style>

<i18n lang="json5">
{
"en": {
// This is the title at the top of a pop-up. It refers to the version of ODK
// Central that the user is using.
"title": "Central Version",
"shortVersion": "You are using ODK Central {version}.",
"longVersion": "You can find more detailed version information below:"
}
}
</i18n>
12 changes: 9 additions & 3 deletions src/components/navbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,17 @@ except according to the terms contained in the LICENSE file.
{{ $t('analyticsNotice') }}
</a>
<ul class="nav navbar-nav">
<navbar-help-dropdown/>
<navbar-help-dropdown @show-version="versionModal.show()"/>
<navbar-locale-dropdown/>
<navbar-actions/>
</ul>
</div>
</div>
</div>
</nav>

<central-version-component v-if="centralVersion.dataExists"
v-bind="versionModal" @hide="versionModal.hide()"/>
<analytics-introduction v-if="config.loaded && config.showsAnalytics"
v-bind="analyticsIntroduction" @hide="analyticsIntroduction.hide()"/>
</div>
Expand All @@ -48,6 +51,7 @@ except according to the terms contained in the LICENSE file.
<script>
import { defineAsyncComponent } from 'vue';

import CentralVersion from './central-version.vue';
import NavbarActions from './navbar/actions.vue';
import NavbarHelpDropdown from './navbar/help-dropdown.vue';
import NavbarLinks from './navbar/links.vue';
Expand All @@ -62,6 +66,7 @@ export default {
name: 'Navbar',
components: {
AnalyticsIntroduction: defineAsyncComponent(loadAsync('AnalyticsIntroduction')),
CentralVersionComponent: CentralVersion,
NavbarActions,
NavbarHelpDropdown,
NavbarLinks,
Expand All @@ -71,12 +76,13 @@ export default {
setup() {
// The component does not assume that this data will exist when the
// component is created.
const { currentUser, analyticsConfig } = useRequestData();
const { currentUser, analyticsConfig, centralVersion } = useRequestData();
const { canRoute } = useRoutes();
return { currentUser, analyticsConfig, canRoute };
return { currentUser, analyticsConfig, centralVersion, canRoute };
},
data() {
return {
versionModal: modalData(),
analyticsIntroduction: modalData('AnalyticsIntroduction')
};
},
Expand Down
15 changes: 14 additions & 1 deletion src/components/navbar/help-dropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ except according to the terms contained in the LICENSE file.
<a href="https://forum.getodk.org/" target="_blank">{{ $t('common.forum') }}</a>
</li>
<li>
<a href="/version.txt" target="_blank">{{ $t('common.version') }}</a>
<a href="/version.txt" target="_blank" @click="showVersion">
sadiqkhoja marked this conversation as resolved.
Show resolved Hide resolved
{{ $t('common.version') }}
</a>
</li>
</ul>
</li>
Expand All @@ -32,9 +34,20 @@ except according to the terms contained in the LICENSE file.
<script setup>
import DocLink from '../doc-link.vue';

import { useRequestData } from '../../request-data';

defineOptions({
name: 'NavbarHelpDropdown'
});
const emit = defineEmits(['show-version']);

const { centralVersion } = useRequestData();
const showVersion = (event) => {
if (centralVersion.dataExists) {
event.preventDefault();
emit('show-version');
}
};
</script>

<i18n lang="json5">
Expand Down
13 changes: 10 additions & 3 deletions src/components/selectable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,18 @@ including this file, may be copied, modified, propagated, or distributed
except according to the terms contained in the LICENSE file.
-->
<template>
<div ref="el" class="selectable" @click="select"><!-- eslint-disable-line vuejs-accessibility/click-events-have-key-events, vue/multiline-html-element-content-newline -->
<div ref="el" class="selectable" :class="{ scroll: !wrap }" @click="select"><!-- eslint-disable-line vuejs-accessibility/click-events-have-key-events, vue/multiline-html-element-content-newline -->
<slot></slot>
</div>
</template>

<script setup>
import { ref } from 'vue';

defineProps({
wrap: Boolean
});

const el = ref(null);
const select = () => {
const selection = getSelection();
Expand All @@ -31,7 +35,10 @@ const select = () => {

.selectable {
font-family: $font-family-monospace;
overflow-x: auto;
white-space: nowrap;

&.scroll {
overflow-x: auto;
white-space: nowrap;
}
}
</style>
62 changes: 62 additions & 0 deletions src/composables/central-version.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
Copyright 2024 ODK Central Developers
See the NOTICE file at the top-level directory of this distribution and at
https://github.com/getodk/central-frontend/blob/master/NOTICE.

This file is part of ODK Central. It is subject to the license terms in
the LICENSE file found in the top-level directory of this distribution and at
https://www.apache.org/licenses/LICENSE-2.0. No part of ODK Central,
including this file, may be copied, modified, propagated, or distributed
except according to the terms contained in the LICENSE file.
*/
import { F } from 'ramda';
import { watchEffect, watchSyncEffect } from 'vue';

import useCallWait from './call-wait';
import { memoizeForContainer } from '../util/composable';
import { useRequestData } from '../request-data';

export default memoizeForContainer(({ i18n, alert, config }) => {
const { createResource } = useRequestData();
const centralVersion = createResource('centralVersion');
watchSyncEffect(() => {
if (!config.dataExists) return;
const versionText = config.centralVersion;
if (versionText == null) return;
centralVersion.data = {
versionText,
currentVersion: versionText.match(/\(v(\d{4}[^-]*)/)[1],
currentDate: config.currentDate
};
});

// Check for a change to /version.txt.
const latestVersion = createResource('latestVersion');
const { callWait } = useCallWait();
// Alerts the user about a version change, then keep alerting them. One
// benefit of this approach is that the user should see the alert even if
// there is another alert (say, about session expiration).
const alertAboutChange = () => {
callWait(
'centralVersion.alert',
() => { alert.info(i18n.t('alert.versionChange')); },
(count) => (count === 0 ? 0 : 60000)
);
};
watchEffect(() => {
if (!centralVersion.dataExists) return;
callWait(
'centralVersion.check',
() => latestVersion.request({ url: '/version.txt', alert: false })
.then(() => {
if (latestVersion.data === centralVersion.versionText) return false;
alertAboutChange();
return true;
})
.catch(F),
() => 60000
);
});

return centralVersion;
});
3 changes: 3 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ except according to the terms contained in the LICENSE file.
// These are the default config values. They will be merged with the response
// for /client-config.json.
export default {
centralVersion: process.env.NODE_ENV === 'development'
? `(v${new Date().getFullYear()}.1.0-sha)\nNote: fake version for development.`
: null,
// `true` to allow navigation to /system/analytics and `false` not to.
showsAnalytics: true,
home: {
Expand Down
5 changes: 3 additions & 2 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,16 @@ import { createApp } from 'vue';
// components' styles.
import './styles';

import App from './components/app.vue';

import createContainer from './container';
import vTooltip from './directives/tooltip';
// ./jquery must be imported before any of Bootstrap's JavaScript plugins,
// because the plugins require jQuery.
import './jquery';
import './bootstrap';

// App must be imported after the Bootstrap modal plugin.
import App from './components/app.vue';

createApp(App)
.use(createContainer())
.directive('tooltip', vTooltip)
Expand Down
18 changes: 7 additions & 11 deletions src/request-data/resources.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,15 @@ export default (container, createResource) => {
createResource('config', (config) => ({
// If client-config.json is completely invalid JSON, `data` seems to be a
// string (e.g., '{]').
transformResponse: ({ data }) => (typeof data === 'object' && data != null
? mergeDeepLeft(data, configDefaults)
: configDefaults),
transformResponse: ({ data, headers }) => {
const result = typeof data === 'object' && data != null
? mergeDeepLeft(data, configDefaults)
: configDefaults;
result.currentDate = new Date(headers.get('date'));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to keep currentDate somewhat current. May be later on we can update this variable (or move to a provide/inject) whenever there is any API response from the backend.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that idea! I think I could fit that in this PR pretty easily.

return result;
},
loaded: computed(() => config.dataExists && config.loadError == null)
}));
createResource('centralVersion', () => ({
transformResponse: ({ data, headers }) =>
shallowReactive({
versionText: data,
currentVersion: data.match(/\(v(\d{4}[^-]*)/)[1],
currentDate: new Date(headers.get('date'))
})
}));
createResource('analyticsConfig', noargs(setupOption));
createResource('roles', (roles) => ({
bySystem: computeIfExists(() => {
Expand Down
1 change: 0 additions & 1 deletion src/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -774,7 +774,6 @@ const routesByName = new Map();
requestData.session,
currentUser,
config,
requestData.centralVersion,
requestData.analyticsConfig,
requestData.roles
]);
Expand Down
2 changes: 1 addition & 1 deletion src/util/session.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ const requestLogout = ({ i18n, requestData, alert, http }) => {
// Resets requestData, clearing data and canceling requests. Some general/system
// resources are not reset.
const resetRequestData = (requestData) => {
const preserve = new Set([requestData.config, requestData.centralVersion]);
const preserve = new Set([requestData.config]);
for (const resource of requestData.resources) {
if (!preserve.has(resource)) resource.reset();
}
Expand Down
Loading