diff --git a/CHANGELOG.md b/CHANGELOG.md index fbcb29c87..e625863ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file. The format ## [UNRELEASED] +### Removed + +- The Google Sheets integration has been completely removed, both for key + results and measurements. Please refer to our API documentation for + instructions on how to update progression programmatically, e.g. by using + Google Apps Script for Google Sheets. + ### Fixed - Fixed a bug that made organization admins unable to add new users. diff --git a/README.md b/README.md index bcede6264..e4b6a9f93 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,6 @@ - [Create Google Cloud API Gateway](#create-google-cloud-api-gateway) - [Build and deploy](#build-and-deploy) - [Lint and fix](#lint-and-fix) - - [Google Sheets integration](#google-sheets-integration) - [Import production data from Cloud Firestore to local Firestore](#import-production-data-from-cloud-firestore-to-local-firestore) - [Requirements](#requirements) - [Export production data](#export-production-data) @@ -72,13 +71,10 @@ Follow this guide to set up a new clean instance of the OKR-tracker. Please read - From the **Project Overview**, select **Service accounts** - Click **Generate new private key** -This key is used for fetching data from Google Sheets (for automatically updating key results). In order to fetch data from Google Sheets, you must set up environment variables for Firebase Functions: - ```bash firebase functions:config:set service_account="" storage.bucket="" - sheets.impersonator="email-address" (optional) ``` Cat the whole service account private key json file into the environment key `service_account`. @@ -124,7 +120,6 @@ Get your Firebase SDK snippet from your [Firebase Console](https://console.fireb | `VITE_STORAGE_BUCKET` | _from SDK snippet_ | | `VITE_MESSAGING_SENDER_ID` | _from SDK snippet_ | | `VITE_APP_ID` | _from SDK snippet_ | -| `VITE_SHEETS_SERVICE_ACCOUNT` | \ | | `VITE_I18N_LOCALE` | `nb-NO OR en-US` | | `VITE_REGION` | `europe-west2` | | `VITE_LOGIN_PROVIDERS` | login providers allowed separated with hyphen - only implemented google, email. Ex: `google-email` | @@ -276,12 +271,6 @@ npm run lint:style # Run style linter npm run lint:style:fix # Fix lint issues found in styles ``` -## Google Sheets integration - -If you want to use Google Sheets API for automatic key results or automatic KPIs, you will need to enable the Google Sheets API in Google Cloud Console. - -If you are using Team Drives with domain-policy (only specific domains have access) then you need to turn on domain-wide delegation on your service accounts and then give that service account access through G Suite Admin. Read more about it [here](https://developers.google.com/identity/protocols/oauth2/service-account#delegatingauthority) - ## Import production data from Cloud Firestore to local Firestore Based on [this tutorial](https://medium.com/firebase-developers/how-to-import-production-data-from-cloud-firestore-to-the-local-emulator-e82ae1c6ed8) with a few differences for our use case. diff --git a/functions/automatedKeyResults.js b/functions/automatedKeyResults.js deleted file mode 100644 index 69263c19e..000000000 --- a/functions/automatedKeyResults.js +++ /dev/null @@ -1,82 +0,0 @@ -import { getFirestore } from 'firebase-admin/firestore'; -import functions from 'firebase-functions'; - -import getSheetsData from './util/getSheetsData.js'; -import config from './config.js'; - -/** - * Scheduled function that automatically updates the progress for all key results - * with the `auto` property set to true, getting the data from the provided - * google sheets details. - */ -export const fetchAutomatedKeyResOnSchedule = functions - .runWith(config.runtimeOpts) - .region(config.region) - .pubsub.schedule(config.autoKeyResFetchFrequency) - .timeZone(config.timeZone) - .onRun(() => { - const db = getFirestore(); - return db - .collection('keyResults') - .where('archived', '==', false) - .where('auto', '==', true) - .get() - .then((snapshot) => snapshot.docs.map(({ id }) => updateAutomaticKeyResult(id))) - .catch((e) => { - throw new Error(e); - }); - }); - -/** - * Manually trigger the scheduled function - */ -export const triggerScheduledFunction = functions - .runWith(config.runtimeOpts) - .region(config.region) - .https.onCall(updateAutomaticKeyResult); - -/** - * Finds the value from the saved sheets data and creates a progress entry for the provided key result id - * @param {string} id - * @returns {number} - */ -async function updateAutomaticKeyResult(id) { - const db = getFirestore(); - const docRef = db.doc(`keyResults/${id}`); - const progressRef = docRef.collection(`progress`); - - try { - const { sheetId, sheetUrl, sheetName, sheetCell } = await docRef - .get() - .then((d) => d.data()); - - if (!(sheetId || sheetUrl) || !sheetName || !sheetCell) { - throw new Error('Missing Sheets details'); - } - - const value = await getSheetsData({ sheetId, sheetUrl, sheetName, sheetCell }); - - if (value === null || value === undefined) { - throw new Error('Data not found'); - } - - // eslint-disable-next-line no-restricted-globals - if (isNaN(value)) { - throw new Error('Invalid data format'); - } - - await progressRef.add({ - created: new Date(), - archived: false, - createdBy: 'auto', - value, - timestamp: new Date(), - }); - await docRef.update({ valid: true, error: false }); - - return value; - } catch ({ message }) { - await docRef.update({ valid: false, error: message }); - throw new functions.https.HttpsError('cancelled', message); - } -} diff --git a/functions/config.js b/functions/config.js index 170eab730..68f529811 100644 --- a/functions/config.js +++ b/functions/config.js @@ -1,7 +1,5 @@ export default { region: 'europe-west2', - autoKpiFetchFrequency: '45 6,18 * * *', - autoKeyResFetchFrequency: '35 7 * * *', backupFrequency: '45 2 * * *', runtimeOpts: { timeoutSeconds: 300, diff --git a/functions/index.js b/functions/index.js index f5720cd43..4e17c3cdb 100644 --- a/functions/index.js +++ b/functions/index.js @@ -18,17 +18,7 @@ initializeApp({ // */ export { automatedBackups } from './backupAndRestore.js'; export { automatedRestore } from './backupAndRestore.js'; -/** - * Scheduled function that automatically updates the progress for all key results - * with the `auto` property set to true, getting the data from the provided - * google sheets details. - */ -export { fetchAutomatedKeyResOnSchedule } from './automatedKeyResults.js'; -export { triggerScheduledFunction } from './automatedKeyResults.js'; -export { fetchKpiDataOnCreate } from './kpi/index.js'; -export { fetchKpiDataOnSchedule } from './kpi/index.js'; -export { fetchKpiDataTrigger } from './kpi/index.js'; export { handleKpiProgress }; export { handleKpiGoals }; diff --git a/functions/kpi/fetchKpiData.js b/functions/kpi/fetchKpiData.js deleted file mode 100644 index 72596e324..000000000 --- a/functions/kpi/fetchKpiData.js +++ /dev/null @@ -1,41 +0,0 @@ -import { FieldValue } from 'firebase-admin/firestore'; -import getSheetsData from '../util/getSheetsData.js'; - -const fetchKpiDataOnUpdate = async (doc) => { - if (!doc || !doc.ref || !doc.ref.update) { - throw new Error('Invalid document'); - } - - const { ref } = doc; - - try { - const { api, auto, sheetId, sheetUrl, sheetName, sheetCell } = doc.data(); - // Some KPI objects might not include the `auto` property. The Google - // Sheets integration was previously enabled in cases where the API was not - // enabled. For backwards compatibility, check for this condition as well. - const sheetsEnabled = - auto || - (auto === undefined && !api && (sheetId || sheetUrl) && sheetName && sheetCell); - - if (!sheetsEnabled) { - await ref.update({ error: FieldValue.delete(), valid: true }); - return true; - } - - const value = await getSheetsData({ sheetId, sheetUrl, sheetName, sheetCell }); - - // eslint-disable-next-line no-restricted-globals - if (!value || isNaN(value)) { - return ref.update({ error: 'Invalid data returned' }); - } - - await ref.collection('progress').add({ value, timestamp: new Date() }); - await ref.update({ error: FieldValue.delete(), currentValue: value, valid: true }); - - return true; - } catch ({ message }) { - return ref.update({ error: message, valid: false }); - } -}; - -export default fetchKpiDataOnUpdate; diff --git a/functions/kpi/index.js b/functions/kpi/index.js deleted file mode 100644 index 14610d81d..000000000 --- a/functions/kpi/index.js +++ /dev/null @@ -1,43 +0,0 @@ -import { getFirestore } from 'firebase-admin/firestore'; -import functions from 'firebase-functions'; -import config from '../config.js'; - -import fetchKpiData from './fetchKpiData.js'; - -export const fetchKpiDataOnCreate = functions - .runWith(config.runtimeOpts) - .region(config.region) - .firestore.document(`kpis/{documentId}`) - .onCreate(fetchKpiData); - -export const fetchKpiDataOnSchedule = functions - .runWith(config.runtimeOpts) - .region(config.region) - .pubsub.schedule(config.autoKpiFetchFrequency) - .timeZone(config.timeZone) - .onRun(() => { - const db = getFirestore(); - - return db - .collection('kpis') - .where('archived', '==', false) - .get() - .then((list) => list.docs.map(fetchKpiData)) - .catch((e) => { - throw new Error(e); - }); - }); - -export const fetchKpiDataTrigger = functions - .runWith(config.runtimeOpts) - .region(config.region) - .https.onCall((id) => { - return getFirestore() - .collection('kpis') - .doc(id) - .get() - .then(fetchKpiData) - .catch((e) => { - throw new Error(e); - }); - }); diff --git a/functions/package-lock.json b/functions/package-lock.json index 0ea312235..c4a668076 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -16,7 +16,6 @@ "firebase-admin": "^11.4.1", "firebase-functions": "^3.24.1", "google-auth-library": "^7.11.0", - "googleapis": "^92.0.0", "morgan": "^1.10.0" }, "devDependencies": { @@ -1724,57 +1723,6 @@ "node": ">=10" } }, - "node_modules/googleapis": { - "version": "92.0.0", - "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-92.0.0.tgz", - "integrity": "sha512-5HgJg7XvqEEJ+GO+2gvnzd5cAcDuSS/VB6nW7thoyj2GMq9nH4VvJwncSevinjLCnv06a+VSxrXNiL5vePHojA==", - "dependencies": { - "google-auth-library": "^7.0.2", - "googleapis-common": "^5.0.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/googleapis-common": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-5.1.0.tgz", - "integrity": "sha512-RXrif+Gzhq1QAzfjxulbGvAY3FPj8zq/CYcvgjzDbaBNCD6bUl+86I7mUs4DKWHGruuK26ijjR/eDpWIDgNROA==", - "dependencies": { - "extend": "^3.0.2", - "gaxios": "^4.0.0", - "google-auth-library": "^7.14.0", - "qs": "^6.7.0", - "url-template": "^2.0.8", - "uuid": "^8.0.0" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/googleapis-common/node_modules/gaxios": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.3.3.tgz", - "integrity": "sha512-gSaYYIO1Y3wUtdfHmjDUZ8LWaxJQpiavzbF5Kq53akSzvmVg0RfyOcFDbO1KJ/KCGRFz2qG+lS81F0nkr7cRJA==", - "dependencies": { - "abort-controller": "^3.0.0", - "extend": "^3.0.2", - "https-proxy-agent": "^5.0.0", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.7" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/googleapis-common/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -3229,11 +3177,6 @@ "node": ">= 0.8" } }, - "node_modules/url-template": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", - "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==" - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/functions/package.json b/functions/package.json index fb2c8df37..2bace225e 100644 --- a/functions/package.json +++ b/functions/package.json @@ -24,7 +24,6 @@ "firebase-admin": "^11.4.1", "firebase-functions": "^3.24.1", "google-auth-library": "^7.11.0", - "googleapis": "^92.0.0", "morgan": "^1.10.0" }, "devDependencies": { diff --git a/functions/tests/unit/util.test.js b/functions/tests/unit/util.test.js deleted file mode 100644 index 4290296d1..000000000 --- a/functions/tests/unit/util.test.js +++ /dev/null @@ -1,18 +0,0 @@ -import sheetIdFromUrl from '../../util/util'; - -describe('Function utils', () => { - test('gets the Google Sheet ID from a URL', () => { - expect(sheetIdFromUrl('docs.google.com/spreadsheets/d/foo')).toEqual('foo'); - expect(sheetIdFromUrl('docs.google.com/spreadsheets/d/foo/edit#gid=123')).toEqual( - 'foo' - ); - expect(sheetIdFromUrl('http://docs.google.com/spreadsheets/d/foo')).toEqual('foo'); - expect( - sheetIdFromUrl('http://docs.google.com/spreadsheets/d/foo/edit#gid=123') - ).toEqual('foo'); - expect(sheetIdFromUrl('https://docs.google.com/spreadsheets/d/foo')).toEqual('foo'); - expect( - sheetIdFromUrl('https://docs.google.com/spreadsheets/d/foo/edit#gid=123') - ).toEqual('foo'); - }); -}); diff --git a/functions/util/getSheetsData.js b/functions/util/getSheetsData.js deleted file mode 100644 index 094c6ee3c..000000000 --- a/functions/util/getSheetsData.js +++ /dev/null @@ -1,69 +0,0 @@ -import functions from 'firebase-functions'; -import googleApis from 'googleapis'; -import sheetIdFromUrl from './util.js'; - -const { google } = googleApis; -const scopes = ['https://www.googleapis.com/auth/spreadsheets']; -const sheetsEmail = - process.env.SHEETS_EMAIL || functions.config().service_account.client_email; -const sheetsKey = - process.env.SHEETS_KEY || functions.config().service_account.private_key; -const sheetsImpersonator = - process.env.SHEETS_IMPERSONATOR || functions.config().sheets?.impersonator || null; - -const jwtClient = new google.auth.JWT( - sheetsEmail, - null, - sheetsKey, - scopes, - sheetsImpersonator -); - -jwtClient.authorize((err) => { - if (err) { - console.error(err); - } -}); - -/** - * Return a value from a Google Sheets cell. - * @param {String} sheetId - ID of the Google Sheets document - * @param {String} sheetUrl - URL of the Google Sheets document - * @param {String} sheetName - Name of sheet (tab) - * @param {String} sheetCell - Cell name to look up - * @returns {Number} - Value of the cell - */ -const getSheetsData = async ({ sheetId, sheetUrl, sheetName, sheetCell }) => { - const sheets = google.sheets('v4'); - if (!(sheetId || sheetUrl) || !sheetName || !sheetCell) { - return false; - } - - if (sheetUrl) { - sheetId = sheetIdFromUrl(sheetUrl); - } - - const sheetRequest = { - auth: jwtClient, - spreadsheetId: sheetId, - range: `${sheetName}!${sheetCell}`, - }; - - return sheets.spreadsheets.values - .get(sheetRequest) - .then((response) => { - try { - return +response.data.values[0][0]; - } catch { - throw new Error(`Cannot find data in cell ${sheetCell} `); - } - }) - .catch((err) => { - if (err.response) { - throw new Error(err.response.status); - } - throw err; - }); -}; - -export default getSheetsData; diff --git a/functions/util/util.js b/functions/util/util.js deleted file mode 100644 index 3dbf20f3b..000000000 --- a/functions/util/util.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Return the ID part of a Google Sheets URL. - */ -export default function sheetIdFromUrl(url) { - const rest = url.replace(/^(https?:\/\/)?docs\.google\.com\/spreadsheets\/d\//, ''); - const sliceAt = rest.indexOf('/'); - return sliceAt === -1 ? rest : rest.slice(0, sliceAt); -} diff --git a/public/help.md b/public/help.md index 323d08e30..c025cfea8 100644 --- a/public/help.md +++ b/public/help.md @@ -224,14 +224,8 @@ Nøkkelresultater er det du _måler_ for et spesifikt [mål](#mål). Kun administratorer og teammedlemmer har rettigheter til å registrere progresjon for et nøkkelresultat. -Det finnes flere måter å registrere progresjon for et nøkkelresultat på: - -- Ved å bytte til `Detaljer`-visning på organisasjonen/produktområdet/produktet - og klikke på fremdriftsvisningen der. -- Direkte inne på detaljsiden for et nøkkelresultatsiden. -- Via API-et. Instruksjoner for dette finner du når du redigerer eller oppretter - et nøkkelresultat. -- Ved å sette opp en [Google Sheets-integrasjon](#google-sheets-integrasjon). +Progresjon registreres via detaljsiden for et nøkkelresultat. Man kan også +registrere progresjon ved bruk av API-et. ### Slette eller endre progresjon for et nøkkelresultat @@ -268,39 +262,12 @@ progresjonen satt til 100 %. - Velg «Målinger». - Klikk «Legg til måling». - Fyll ut detaljene i skjemaet og klikk «Lagre». -- (Valgfritt) Sett opp [Google Sheets-integrasjon](#google-sheets-integrasjon). ### Oppdatere måleverdier -Måleverdiene kan oppdateres på tre ulike måter: +Måleverdiene kan oppdateres på to ulike måter: 1. Direkte i OKR-trackeren ved å benytte knappen «Legg til målepunkt». 2. Via API-et. Instruksjoner for dette finner du når du redigerer eller oppretter en ny måling. - -3. Ved å knytte målingen til et Google Sheets-dokument vil verdier fra - dokumentet lastes inn automatisk i OKR-trackeren to ganger daglig. Google - Sheets-dokumentet må deles med OKR-trackerens _service account_. Adressen til - denne finner du når du redigerer eller oppretter en ny måling. - ---- - -## Google Sheets-integrasjon - -Med hjelp av Google Sheets kan du konfigurere automatisk oppdatering av -progresjon for nøkkelresultater og målinger. - -Progresjonen for det «automatiske» nøkkelresultatet eller målingen vil bli -oppdatert to ganger om dagen med verdien i den _cellen_ som er definert. - -| Felt | Beskrivelse | -|-------------------|------------------------------------------------------------| -| Google Sheets URL | Den fullstendige nettadressen til Google Sheets-dokumentet | -| Fane | Navnet på fanen hvor cellen ligger | -| Celle | Navnet på cellen (for eksempel «A1») | - -Dokumentet må deles med OKR-trackeren via applikasjonens _service account_. - -Trykk til slutt på «Lagre» og vent til du får grønn «OK» for bekreftelse på -gyldig kobling. diff --git a/src/components/KpiDetails.vue b/src/components/KpiDetails.vue index f9549263e..6d492f2b0 100644 --- a/src/components/KpiDetails.vue +++ b/src/components/KpiDetails.vue @@ -19,20 +19,6 @@ :html="kpi.description" /> - - {{ $t('kpi.automation.error') }} - - {{ - $t('kpi.automation.automationLink') - }} - - - import { mapState, mapGetters } from 'vuex'; import { db } from '@/config/firebaseConfig'; -import { PktAlert, PktButton } from '@oslokommune/punkt-vue2'; +import { PktButton } from '@oslokommune/punkt-vue2'; import { filterDuplicatedProgressValues, getCachedKPIProgress, @@ -85,7 +71,6 @@ export default { name: 'KpiDetails', components: { - PktAlert, PktButton, ProgressModal, HTMLOutput, diff --git a/src/components/drawers/KpiDrawer.vue b/src/components/drawers/KpiDrawer.vue index cb172c11b..354529538 100644 --- a/src/components/drawers/KpiDrawer.vue +++ b/src/components/drawers/KpiDrawer.vue @@ -16,21 +16,6 @@