mirror of
https://github.com/bellingcat/auto-archiver-setup-tool.git
synced 2026-06-12 21:48:37 +03:00
final updates before release
This commit is contained in:
@@ -1,96 +1,98 @@
|
|||||||
/**
|
//NB: this code has been disabled since the cronjob is now handled by the API
|
||||||
* Import function triggers from their respective submodules:
|
|
||||||
*
|
|
||||||
* const {onCall} = require("firebase-functions/v2/https");
|
|
||||||
* const {onDocumentWritten} = require("firebase-functions/v2/firestore");
|
|
||||||
*
|
|
||||||
* See a full list of supported triggers at https://firebase.google.com/docs/functions
|
|
||||||
*/
|
|
||||||
|
|
||||||
const { onSchedule } = require("firebase-functions/v2/scheduler");
|
// /**
|
||||||
const logger = require("firebase-functions/logger");
|
// * Import function triggers from their respective submodules:
|
||||||
|
// *
|
||||||
|
// * const {onCall} = require("firebase-functions/v2/https");
|
||||||
|
// * const {onDocumentWritten} = require("firebase-functions/v2/firestore");
|
||||||
|
// *
|
||||||
|
// * See a full list of supported triggers at https://firebase.google.com/docs/functions
|
||||||
|
// */
|
||||||
|
|
||||||
// The Firebase Admin SDK to access Firestore.
|
// const { onSchedule } = require("firebase-functions/v2/scheduler");
|
||||||
const { initializeApp } = require("firebase-admin/app");
|
// const logger = require("firebase-functions/logger");
|
||||||
const { getFirestore } = require("firebase-admin/firestore");
|
|
||||||
|
|
||||||
const { defineSecret } = require('firebase-functions/params');
|
// // The Firebase Admin SDK to access Firestore.
|
||||||
const API_TOKEN = defineSecret('API_SERVICE_PASSWORD');
|
// const { initializeApp } = require("firebase-admin/app");
|
||||||
const CLIENT_EMAIL = defineSecret('GOOGLE_API_CLIENT_EMAIL');
|
// const { getFirestore } = require("firebase-admin/firestore");
|
||||||
const PRIVATE_KEY = defineSecret('GOOGLE_API_PRIVATE_KEY');
|
|
||||||
|
|
||||||
const { google } = require('googleapis');
|
// const { defineSecret } = require('firebase-functions/params');
|
||||||
|
// const API_TOKEN = defineSecret('API_SERVICE_PASSWORD');
|
||||||
|
// const CLIENT_EMAIL = defineSecret('GOOGLE_API_CLIENT_EMAIL');
|
||||||
|
// const PRIVATE_KEY = defineSecret('GOOGLE_API_PRIVATE_KEY');
|
||||||
|
|
||||||
initializeApp();
|
// const { google } = require('googleapis');
|
||||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
||||||
|
|
||||||
String.prototype.hashCode = function () {
|
// initializeApp();
|
||||||
// https://stackoverflow.com/a/7616484/6196010
|
// const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||||
// Generating 1M random strings and applying this function shows it's very balanced for modulo 60
|
|
||||||
// 0 has double frequency of other numbers, but that's not a problem
|
|
||||||
var hash = 0,
|
|
||||||
i, chr;
|
|
||||||
if (this.length === 0) return hash;
|
|
||||||
for (i = 0; i < this.length; i++) {
|
|
||||||
chr = this.charCodeAt(i);
|
|
||||||
hash = ((hash << 5) - hash) + chr;
|
|
||||||
hash |= 0; // Convert to 32bit integer
|
|
||||||
}
|
|
||||||
return hash;
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO: disable the scheduler
|
// String.prototype.hashCode = function () {
|
||||||
exports.processSheetScheduler = onSchedule(
|
// // https://stackoverflow.com/a/7616484/6196010
|
||||||
{ secrets: [API_TOKEN, CLIENT_EMAIL, PRIVATE_KEY], schedule: "* * * * *" },
|
// // Generating 1M random strings and applying this function shows it's very balanced for modulo 60
|
||||||
async (event) => {
|
// // 0 has double frequency of other numbers, but that's not a problem
|
||||||
// authenticate the service account
|
// var hash = 0,
|
||||||
const googleAuth = new google.auth.JWT(CLIENT_EMAIL.value(), null, PRIVATE_KEY.value().replace(/\\n/g, '\n'), 'https://www.googleapis.com/auth/spreadsheets');
|
// i, chr;
|
||||||
const sheets = await google.sheets({ version: 'v4', auth: googleAuth });
|
// if (this.length === 0) return hash;
|
||||||
|
// for (i = 0; i < this.length; i++) {
|
||||||
|
// chr = this.charCodeAt(i);
|
||||||
|
// hash = ((hash << 5) - hash) + chr;
|
||||||
|
// hash |= 0; // Convert to 32bit integer
|
||||||
|
// }
|
||||||
|
// return hash;
|
||||||
|
// }
|
||||||
|
|
||||||
// get all documents from firestore sheets collection
|
// //TODO: disable the scheduler
|
||||||
const db = getFirestore();
|
// exports.processSheetScheduler = onSchedule(
|
||||||
|
// { secrets: [API_TOKEN, CLIENT_EMAIL, PRIVATE_KEY], schedule: "* * * * *" },
|
||||||
|
// async (event) => {
|
||||||
|
// // authenticate the service account
|
||||||
|
// const googleAuth = new google.auth.JWT(CLIENT_EMAIL.value(), null, PRIVATE_KEY.value().replace(/\\n/g, '\n'), 'https://www.googleapis.com/auth/spreadsheets');
|
||||||
|
// const sheets = await google.sheets({ version: 'v4', auth: googleAuth });
|
||||||
|
|
||||||
// each sheet runs once per hour, so we hash the sheet id and only process it if the hash % 60 matches the cron minute
|
// // get all documents from firestore sheets collection
|
||||||
const querySnapshot = await db.collection("sheets").get();
|
// const db = getFirestore();
|
||||||
const eventDate = new Date(Date.parse(event.scheduleTime));
|
|
||||||
querySnapshot.forEach(async (doc) => {
|
|
||||||
const hashToSixty = Math.abs(doc.id.hashCode() % 60);
|
|
||||||
if (hashToSixty != eventDate.getMinutes()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
logger.log(`processing document ${doc.id}, its hash % 60 (${hashToSixty}) matches the cron minute (${eventDate.getMinutes()})`);
|
|
||||||
|
|
||||||
try {
|
// // each sheet runs once per hour, so we hash the sheet id and only process it if the hash % 60 matches the cron minute
|
||||||
await sheets.spreadsheets.get({ spreadsheetId: doc.data().sheetId });
|
// const querySnapshot = await db.collection("sheets").get();
|
||||||
} catch (e) {
|
// const eventDate = new Date(Date.parse(event.scheduleTime));
|
||||||
if (e.status == 404) {
|
// querySnapshot.forEach(async (doc) => {
|
||||||
await doc.ref.delete();
|
// const hashToSixty = Math.abs(doc.id.hashCode() % 60);
|
||||||
logger.log(`document ${doc.data().sheetId} not found, deleted`);
|
// if (hashToSixty != eventDate.getMinutes()) {
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
}
|
// logger.log(`processing document ${doc.id}, its hash % 60 (${hashToSixty}) matches the cron minute (${eventDate.getMinutes()})`);
|
||||||
|
|
||||||
// send POST request with sheetID to trigger sheet processing
|
// try {
|
||||||
const url = "https://auto-archiver-api.bellingcat.com/sheet_service";
|
// await sheets.spreadsheets.get({ spreadsheetId: doc.data().sheetId });
|
||||||
const data = {
|
// } catch (e) {
|
||||||
sheet_id: doc.data().sheetId,
|
// if (e.status == 404) {
|
||||||
author_id: doc.data().email ?? doc.data().uid,
|
// await doc.ref.delete();
|
||||||
tags: ["setup-tool"]
|
// logger.log(`document ${doc.data().sheetId} not found, deleted`);
|
||||||
};
|
// return;
|
||||||
const options = {
|
// }
|
||||||
method: "POST",
|
// }
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${API_TOKEN.value()}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await fetch(url, options);
|
// // send POST request with sheetID to trigger sheet processing
|
||||||
|
// const url = "https://auto-archiver-api.bellingcat.com/sheet_service";
|
||||||
|
// const data = {
|
||||||
|
// sheet_id: doc.data().sheetId,
|
||||||
|
// author_id: doc.data().email ?? doc.data().uid,
|
||||||
|
// tags: ["setup-tool"]
|
||||||
|
// };
|
||||||
|
// const options = {
|
||||||
|
// method: "POST",
|
||||||
|
// headers: {
|
||||||
|
// "Content-Type": "application/json",
|
||||||
|
// Authorization: `Bearer ${API_TOKEN.value()}`,
|
||||||
|
// },
|
||||||
|
// body: JSON.stringify(data),
|
||||||
|
// };
|
||||||
|
|
||||||
await doc.ref.update({ lastArchived: Date.now() });
|
// const response = await fetch(url, options);
|
||||||
|
|
||||||
await sleep(100);
|
// await doc.ref.update({ lastArchived: Date.now() });
|
||||||
});
|
|
||||||
}
|
// await sleep(100);
|
||||||
);
|
// });
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve --port 8081 --skip-plugins @vue/cli-plugin-eslint",
|
"serve": "vue-cli-service serve --port 8081 --skip-plugins @vue/cli-plugin-eslint",
|
||||||
"build": "vue-cli-service build",
|
"build": "VUE_APP_API_ENDPOINT=https://auto-archiver-api.bellingcat.com vue-cli-service build --skip-plugins @vue/cli-plugin-eslint",
|
||||||
|
"preview": "serve -s dist -l 8081",
|
||||||
"lint": "vue-cli-service lint"
|
"lint": "vue-cli-service lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -35,6 +36,7 @@
|
|||||||
"firebase-admin": "^11.11.1",
|
"firebase-admin": "^11.11.1",
|
||||||
"firebase-functions": "^4.5.0",
|
"firebase-functions": "^4.5.0",
|
||||||
"prettier": "^2.4.1",
|
"prettier": "^2.4.1",
|
||||||
|
"serve": "^14.2.4",
|
||||||
"vue-template-compiler": "^2.6.14"
|
"vue-template-compiler": "^2.6.14"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
32
src/App.vue
32
src/App.vue
@@ -13,17 +13,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12">
|
<v-col cols="12">
|
||||||
You can deploy your own version of this tool by hosting the <a href="https://github.com/bellingcat/auto-archiver-api">API</a> and the <a href="https://github.com/bellingcat/auto-archiver-api">UI</a>.
|
You can deploy your own version of this tool by hosting the
|
||||||
<br/>
|
<a href="https://github.com/bellingcat/auto-archiver-api">API</a> and
|
||||||
This tool uses <a href="https://github.com/bellingcat/auto-archiver">Bellingcat's Auto Archiver</a> under the hood to archive online content.
|
the
|
||||||
<br/>
|
<a href="https://github.com/bellingcat/auto-archiver-setup-tool">UI</a
|
||||||
For more information about it see <a
|
>.
|
||||||
href="https://www.bellingcat.com/resources/2022/09/22/preserve-vital-online-content-with-bellingcats-auto-archiver-tool/">associated
|
<br />
|
||||||
article</a>.
|
This tool uses
|
||||||
|
<a href="https://github.com/bellingcat/auto-archiver"
|
||||||
|
>Bellingcat's Auto Archiver</a
|
||||||
|
>
|
||||||
|
under the hood to archive online content.
|
||||||
|
<br />
|
||||||
|
For more information about it see
|
||||||
|
<a
|
||||||
|
href="https://www.bellingcat.com/resources/2022/09/22/preserve-vital-online-content-with-bellingcats-auto-archiver-tool/"
|
||||||
|
>associated article</a
|
||||||
|
>.
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
</v-footer>
|
</v-footer>
|
||||||
</v-app>
|
</v-app>
|
||||||
</template>
|
</template>
|
||||||
@@ -96,14 +104,14 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
background-color: rgba(0, 0, 0, .1);
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
padding: .2em .4em;
|
padding: 0.2em 0.4em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.v-card .v-card-text {
|
.v-card .v-card-text {
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 1.7rem;
|
line-height: 1.7rem;
|
||||||
letter-spacing: .0092em;
|
letter-spacing: 0.0092em;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,210 +1,320 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-row class="my-2">
|
<v-row class="my-2">
|
||||||
<v-col cols="12" sm="12" class="ma-0 pb-0">
|
<v-col cols="12" sm="12" class="ma-0 pb-0">
|
||||||
<v-text-field label="Google Sheets document name" v-model="sheetName" required
|
<v-text-field
|
||||||
density="comfortable"></v-text-field>
|
label="Google Sheets document name"
|
||||||
</v-col>
|
v-model="sheetName"
|
||||||
<v-col v-if="!actionIsCreate" cols="12" sm="12" class="ma-0 py-0">
|
required
|
||||||
<v-text-field label="Existing Google Sheet URL/ID" v-model="sheetUrlId" required density="comfortable">
|
density="comfortable"
|
||||||
</v-text-field>
|
></v-text-field>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="6" sm="6" class="ma-0 py-0">
|
<v-col v-if="!actionIsCreate" cols="12" sm="12" class="ma-0 py-0">
|
||||||
<v-select v-model="group" label="Group" :items="availableGroups" required density="comfortable"></v-select>
|
<v-text-field
|
||||||
</v-col>
|
label="Existing Google Sheet URL/ID"
|
||||||
<v-col cols="6" sm="6" class="ma-0 py-0">
|
v-model="sheetUrlId"
|
||||||
<v-select v-model="frequency" label="Archive frequency" :items="availableFrequencies"
|
required
|
||||||
:disabled="!availableFrequencies?.length" required density="comfortable"></v-select>
|
density="comfortable"
|
||||||
</v-col>
|
>
|
||||||
<v-col cols="12" sm="12" class="text-right pt-0">
|
</v-text-field>
|
||||||
<small v-if="spreadsheetId">Detected Spreadsheet id: <code>{{ spreadsheetId }}</code></small>
|
</v-col>
|
||||||
</v-col>
|
<v-col cols="6" sm="6" class="ma-0 py-0">
|
||||||
<v-col cols="12" sm="12" class="text-right pt-0">
|
<v-select
|
||||||
<v-progress-circular color="green" indeterminate class="mx-6" v-if="loading"></v-progress-circular>
|
v-model="group"
|
||||||
<v-btn v-if="newSheetId" :href="`https://docs.google.com/spreadsheets/d/${newSheetId}`"
|
label="Group"
|
||||||
append-icon="mdi-open-in-new" :title="newSheetId" target="_blank" color="success" class="mx-2"
|
:items="availableGroups"
|
||||||
size="large">
|
required
|
||||||
open sheet
|
density="comfortable"
|
||||||
</v-btn>
|
></v-select>
|
||||||
<v-btn v-if="actionIsCreate" color="teal" size="large" :disabled="!requiredData"
|
</v-col>
|
||||||
@click="createSheet">Create</v-btn>
|
<v-col cols="6" sm="6" class="ma-0 py-0">
|
||||||
<v-btn v-if="!actionIsCreate" color="teal" size="large" :disabled="!requiredDataExisting"
|
<v-select
|
||||||
@click="addExistingSheet">Add Existing Sheet</v-btn>
|
v-model="frequency"
|
||||||
</v-col>
|
label="Archive frequency"
|
||||||
<v-col cols="12" sm="12" class="pt-0" v-if="group != 'please select'">
|
:items="availableFrequencies"
|
||||||
<span>
|
:disabled="!availableFrequencies?.length"
|
||||||
<span class="text-medium-emphasis mb-1">
|
required
|
||||||
<strong>{{ group }}</strong>: {{ groupPermissions.description }}
|
density="comfortable"
|
||||||
</span>
|
></v-select>
|
||||||
<ul>
|
</v-col>
|
||||||
<li>
|
<v-col cols="12" sm="12" class="text-right pt-0">
|
||||||
Active sheets:
|
<small v-if="spreadsheetId"
|
||||||
<strong>{{ groupUsage.total_sheets || 0 }}</strong> out of
|
>Detected Spreadsheet id: <code>{{ spreadsheetId }}</code></small
|
||||||
<strong>{{ displayPermissionValue(groupPermissions?.max_sheets, "") }}</strong>
|
>
|
||||||
<v-chip v-if="maxedOutGroupQuota" label class="ml-2" color="red" density="comfortable"
|
</v-col>
|
||||||
size="small">maxed out</v-chip>
|
<v-col cols="12" sm="12" class="text-right pt-0">
|
||||||
</li>
|
<v-progress-circular
|
||||||
<li>Monthly URLs: <strong>{{ groupUsage.monthly_urls || 0 }}</strong> out of <strong>{{
|
color="green"
|
||||||
displayPermissionValue(groupPermissions?.max_monthly_urls, " URLs") }}</strong></li>
|
indeterminate
|
||||||
<li>Monthly MBs: <strong>{{ groupUsage.monthly_mbs || 0 }}</strong> out of <strong>{{
|
class="mx-6"
|
||||||
displayPermissionValue(groupPermissions?.max_monthly_mbs, " MBs") }}</strong></li>
|
v-if="loading"
|
||||||
<li>We will store archives for: <strong>{{
|
></v-progress-circular>
|
||||||
displayPermissionValue(groupPermissions?.max_archive_lifespan_months, " months") }}</strong>
|
<v-btn
|
||||||
</li>
|
v-if="newSheetId"
|
||||||
<li>You <strong>{{ groupPermissions?.manually_trigger_sheet ? "can" : "cannot" }}</strong> manually
|
:href="`https://docs.google.com/spreadsheets/d/${newSheetId}`"
|
||||||
trigger sheets in this group. </li>
|
append-icon="mdi-open-in-new"
|
||||||
</ul>
|
:title="newSheetId"
|
||||||
<p v-if="!actionIsCreate" class="text-medium-emphasis mt-2">
|
target="_blank"
|
||||||
<strong>NOTE:</strong> invite <a :href="`mailto:${groupPermissions?.service_account_email}`">{{
|
color="success"
|
||||||
groupPermissions?.service_account_email }}</a> to the sheet, see further instructions below.
|
class="mx-2"
|
||||||
</p>
|
size="large"
|
||||||
</span>
|
>
|
||||||
</v-col>
|
open sheet
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
v-if="actionIsCreate"
|
||||||
|
color="teal"
|
||||||
|
size="large"
|
||||||
|
:disabled="!requiredData"
|
||||||
|
@click="createSheet"
|
||||||
|
>Create</v-btn
|
||||||
|
>
|
||||||
|
<v-btn
|
||||||
|
v-if="!actionIsCreate"
|
||||||
|
color="teal"
|
||||||
|
size="large"
|
||||||
|
:disabled="!requiredDataExisting"
|
||||||
|
@click="addExistingSheet"
|
||||||
|
>Add Existing Sheet</v-btn
|
||||||
|
>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="12" class="pt-0" v-if="group != 'please select'">
|
||||||
|
<span>
|
||||||
|
<span class="text-medium-emphasis mb-1">
|
||||||
|
<strong>{{ group }}</strong
|
||||||
|
>: {{ groupPermissions.description }}
|
||||||
|
</span>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Active sheets:
|
||||||
|
<strong>{{ groupUsage.total_sheets || 0 }}</strong> out of
|
||||||
|
<strong>{{
|
||||||
|
displayPermissionValue(groupPermissions?.max_sheets, "")
|
||||||
|
}}</strong>
|
||||||
|
<v-chip
|
||||||
|
v-if="maxedOutGroupQuota"
|
||||||
|
label
|
||||||
|
class="ml-2"
|
||||||
|
color="red"
|
||||||
|
density="comfortable"
|
||||||
|
size="small"
|
||||||
|
>maxed out</v-chip
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Monthly URLs:
|
||||||
|
<strong>{{ groupUsage.monthly_urls || 0 }}</strong> out of
|
||||||
|
<strong>{{
|
||||||
|
displayPermissionValue(
|
||||||
|
groupPermissions?.max_monthly_urls,
|
||||||
|
" URLs"
|
||||||
|
)
|
||||||
|
}}</strong>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Monthly MBs: <strong>{{ groupUsage.monthly_mbs || 0 }}</strong> out
|
||||||
|
of
|
||||||
|
<strong>{{
|
||||||
|
displayPermissionValue(groupPermissions?.max_monthly_mbs, " MBs")
|
||||||
|
}}</strong>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
We will store archives for:
|
||||||
|
<strong>{{
|
||||||
|
displayPermissionValue(
|
||||||
|
groupPermissions?.max_archive_lifespan_months,
|
||||||
|
" months"
|
||||||
|
)
|
||||||
|
}}</strong>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
You
|
||||||
|
<strong>{{
|
||||||
|
groupPermissions?.manually_trigger_sheet ? "can" : "cannot"
|
||||||
|
}}</strong>
|
||||||
|
manually trigger sheets in this group.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p v-if="!actionIsCreate" class="text-medium-emphasis mt-2">
|
||||||
|
<strong>NOTE:</strong> invite
|
||||||
|
<a :href="`mailto:${groupPermissions?.service_account_email}`">{{
|
||||||
|
groupPermissions?.service_account_email
|
||||||
|
}}</a>
|
||||||
|
to the sheet, see further instructions below.
|
||||||
|
</p>
|
||||||
|
</span>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<SnackBar
|
||||||
</v-row>
|
:message="snackbarMessage"
|
||||||
|
:show="snackbar"
|
||||||
<SnackBar :message="snackbarMessage" :show="snackbar" :color="snackbarColor" @update:show="snackbar = $event" />
|
:color="snackbarColor"
|
||||||
|
@update:show="snackbar = $event"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import SnackBar from "@/components/SnackBar.vue";
|
import SnackBar from "@/components/SnackBar.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "AddSheet",
|
name: "AddSheet",
|
||||||
components: {
|
components: {
|
||||||
SnackBar,
|
SnackBar,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
actionIsCreate: {
|
actionIsCreate: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true,
|
required: true,
|
||||||
default: true,
|
default: true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
snackbar: false,
|
snackbar: false,
|
||||||
snackbarMessage: "",
|
snackbarMessage: "",
|
||||||
snackbarColor: "red",
|
snackbarColor: "red",
|
||||||
loading: false,
|
loading: false,
|
||||||
|
|
||||||
sheetName: ``.trim(),
|
sheetName: ``.trim(),
|
||||||
sheetUrlId: ``,
|
sheetUrlId: ``,
|
||||||
|
|
||||||
group: "please select",
|
group: "please select",
|
||||||
|
|
||||||
frequency: "please select",
|
frequency: "please select",
|
||||||
|
|
||||||
newSheetId: "",
|
newSheetId: "",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
user() {
|
user() {
|
||||||
return this.$store.state.user;
|
return this.$store.state.user;
|
||||||
},
|
},
|
||||||
requiredData() {
|
requiredData() {
|
||||||
return this.sheetName && this.availableGroups?.some(g => g.value === this.group) && this.availableFrequencies?.some(f => f === this.frequency) && !this.maxedOutGroupQuota;
|
return (
|
||||||
},
|
this.sheetName &&
|
||||||
requiredDataExisting() {
|
this.availableGroups?.some((g) => g.value === this.group) &&
|
||||||
return this.sheetName && this.spreadsheetId && this.availableGroups?.some(g => g.value === this.group) && this.availableFrequencies?.some(f => f === this.frequency) && !this.maxedOutGroupQuota;
|
this.availableFrequencies?.some((f) => f === this.frequency) &&
|
||||||
},
|
!this.maxedOutGroupQuota
|
||||||
availableGroups() {
|
);
|
||||||
const permissions = this.$store.state.user?.permissions || {};
|
},
|
||||||
return Object.keys(permissions)
|
requiredDataExisting() {
|
||||||
.filter(group => group !== "all" && permissions[group].archive_sheet)
|
return (
|
||||||
.map(g => ({ title: g, value: g }));
|
this.sheetName &&
|
||||||
},
|
this.spreadsheetId &&
|
||||||
availableFrequencies() {
|
this.availableGroups?.some((g) => g.value === this.group) &&
|
||||||
return this.$store.state.user?.permissions?.[this.group]?.sheet_frequency || [];
|
this.availableFrequencies?.some((f) => f === this.frequency) &&
|
||||||
},
|
!this.maxedOutGroupQuota
|
||||||
groupPermissions() {
|
);
|
||||||
return this.$store.state.user?.permissions?.[this.group] || {};
|
},
|
||||||
},
|
availableGroups() {
|
||||||
groupUsage() {
|
const permissions = this.$store.state.user?.permissions || {};
|
||||||
return this.$store.state.user?.usage?.["groups"]?.[this.group] || {};
|
return Object.keys(permissions)
|
||||||
},
|
.filter((group) => group !== "all" && permissions[group].archive_sheet)
|
||||||
maxedOutGroupQuota() {
|
.map((g) => ({ title: g, value: g }));
|
||||||
if (this.groupPermissions?.archive_sheet === false) return true;
|
},
|
||||||
if (this.groupPermissions.max_sheets === -1) return false;
|
availableFrequencies() {
|
||||||
return this.groupUsage.total_sheets >= this.groupPermissions.max_sheets;
|
return (
|
||||||
},
|
this.$store.state.user?.permissions?.[this.group]?.sheet_frequency || []
|
||||||
spreadsheetId() {
|
);
|
||||||
if (
|
},
|
||||||
this.sheetUrlId.startsWith("http") &&
|
groupPermissions() {
|
||||||
this.sheetUrlId.split("/").length >= 6
|
return this.$store.state.user?.permissions?.[this.group] || {};
|
||||||
) {
|
},
|
||||||
return this.sheetUrlId.split("/")[5];
|
groupUsage() {
|
||||||
}
|
return this.$store.state.user?.usage?.["groups"]?.[this.group] || {};
|
||||||
return this.sheetUrlId;
|
},
|
||||||
},
|
maxedOutGroupQuota() {
|
||||||
},
|
if (this.groupPermissions?.archive_sheet === false) return true;
|
||||||
watch: {
|
if (this.groupPermissions.max_sheets === -1) return false;
|
||||||
},
|
return this.groupUsage.total_sheets >= this.groupPermissions.max_sheets;
|
||||||
methods: {
|
},
|
||||||
showSnackbar(message, color = "red") {
|
spreadsheetId() {
|
||||||
this.snackbarMessage = message;
|
if (
|
||||||
this.snackbarColor = color;
|
this.sheetUrlId.startsWith("http") &&
|
||||||
this.snackbar = true;
|
this.sheetUrlId.split("/").length >= 6
|
||||||
},
|
) {
|
||||||
createSheet() {
|
return this.sheetUrlId.split("/")[5];
|
||||||
if (!this.requiredData) return;
|
}
|
||||||
if (this.loading) return;
|
return this.sheetUrlId;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {},
|
||||||
|
methods: {
|
||||||
|
showSnackbar(message, color = "red") {
|
||||||
|
this.snackbarMessage = message;
|
||||||
|
this.snackbarColor = color;
|
||||||
|
this.snackbar = true;
|
||||||
|
},
|
||||||
|
createSheet() {
|
||||||
|
if (!this.requiredData) return;
|
||||||
|
if (this.loading) return;
|
||||||
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.newSheetId = "";
|
this.newSheetId = "";
|
||||||
this.$store.dispatch("createSheet", { name: this.sheetName, service_account_email: this.groupPermissions.service_account_email }).then((res) => {
|
this.$store
|
||||||
this.$store.dispatch("checkUserUsage");
|
.dispatch("createSheet", {
|
||||||
if (!res.success) throw new Error(res.result);
|
name: this.sheetName,
|
||||||
this.newSheetId = res.result;
|
service_account_email: this.groupPermissions.service_account_email,
|
||||||
this.addSheetToAPI(this.newSheetId);
|
})
|
||||||
}).catch((error) => {
|
.then((res) => {
|
||||||
console.error(error);
|
this.$store.dispatch("checkUserUsage");
|
||||||
this.showSnackbar(`Unable to create sheet: ${error.message}`);
|
if (!res.success) throw new Error(res.result);
|
||||||
this.loading = false;
|
this.newSheetId = res.result;
|
||||||
});
|
this.addSheetToAPI(this.newSheetId);
|
||||||
},
|
})
|
||||||
addExistingSheet() {
|
.catch((error) => {
|
||||||
if (!this.requiredDataExisting) return;
|
console.error(error);
|
||||||
if (this.loading) return;
|
this.showSnackbar(`Unable to create sheet: ${error.message}`);
|
||||||
this.loading = true;
|
this.loading = false;
|
||||||
this.addSheetToAPI(this.spreadsheetId);
|
});
|
||||||
},
|
},
|
||||||
addSheetToAPI(sheetId) {
|
addExistingSheet() {
|
||||||
fetch(`${this.$store.state.API_ENDPOINT}/sheet/create`, {
|
if (!this.requiredDataExisting) return;
|
||||||
method: "POST",
|
if (this.loading) return;
|
||||||
headers: {
|
this.loading = true;
|
||||||
"Content-Type": "application/json",
|
this.addSheetToAPI(this.spreadsheetId);
|
||||||
Authorization: `Bearer ${this.$store.state.access_token}`,
|
},
|
||||||
},
|
addSheetToAPI(sheetId) {
|
||||||
body: JSON.stringify({
|
fetch(`${this.$store.state.API_ENDPOINT}/sheet/create`, {
|
||||||
id: sheetId,
|
method: "POST",
|
||||||
name: this.sheetName,
|
headers: {
|
||||||
group_id: this.group,
|
"Content-Type": "application/json",
|
||||||
frequency: this.frequency,
|
Authorization: `Bearer ${this.$store.state.access_token}`,
|
||||||
})
|
},
|
||||||
}).then(async response => {
|
body: JSON.stringify({
|
||||||
const j = await response.json();
|
id: sheetId,
|
||||||
if (response.status === 201) {
|
name: this.sheetName,
|
||||||
this.showSnackbar(`Sheet created successfully!`, "green");
|
group_id: this.group,
|
||||||
this.$store.dispatch("getSheets");
|
frequency: this.frequency,
|
||||||
this.$store.dispatch("checkUserUsage");
|
}),
|
||||||
} else {
|
})
|
||||||
throw new Error(JSON.stringify(j));
|
.then(async (response) => {
|
||||||
}
|
const j = await response.json();
|
||||||
}).catch(error => {
|
if (response.status === 201) {
|
||||||
console.error("/sheet/create ", error);
|
this.showSnackbar(`Sheet created successfully!`, "green");
|
||||||
this.showSnackbar(`Unable to save sheet to DB: ${error.message}`);
|
this.$store.dispatch("getSheets");
|
||||||
}).finally(() => {
|
this.$store.dispatch("checkUserUsage");
|
||||||
this.loading = false;
|
} else {
|
||||||
this.sheetName = "";
|
throw new Error(JSON.stringify(j));
|
||||||
this.sheetUrlId = "";
|
}
|
||||||
this.group = "please select";
|
})
|
||||||
});
|
.catch((error) => {
|
||||||
},
|
console.error("/sheet/create ", error);
|
||||||
displayPermissionValue(value, extraWord) {
|
this.showSnackbar(`Unable to save sheet to DB: ${error.message}`);
|
||||||
if (value === undefined) { return "not set"; }
|
})
|
||||||
return value == -1 ? "no limit" : value + extraWord;
|
.finally(() => {
|
||||||
}
|
this.loading = false;
|
||||||
},
|
this.sheetName = "";
|
||||||
|
this.sheetUrlId = "";
|
||||||
|
this.group = "please select";
|
||||||
|
});
|
||||||
|
},
|
||||||
|
displayPermissionValue(value, extraWord) {
|
||||||
|
if (value === undefined) {
|
||||||
|
return "not set";
|
||||||
|
}
|
||||||
|
return value == -1 ? "no limit" : value + extraWord;
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-container class="pane">
|
<v-container class="pane">
|
||||||
<v-card class="pa-0">
|
<v-card class="pa-0">
|
||||||
|
|
||||||
<v-tabs v-model="tab" bg-color="teal" grow class="elevation-1">
|
<v-tabs v-model="tab" bg-color="teal" grow class="elevation-1">
|
||||||
<v-tab v-for="item in items" :key="item" :text="item" :value="item"></v-tab>
|
<v-tab
|
||||||
|
v-for="item in items"
|
||||||
|
:key="item"
|
||||||
|
:text="item"
|
||||||
|
:value="item"
|
||||||
|
></v-tab>
|
||||||
</v-tabs>
|
</v-tabs>
|
||||||
|
|
||||||
<v-tabs-window v-model="tab" class="elevation-1 rounded">
|
<v-tabs-window v-model="tab" class="elevation-1 rounded">
|
||||||
@@ -16,15 +20,16 @@
|
|||||||
<v-expansion-panel-text>
|
<v-expansion-panel-text>
|
||||||
<ol>
|
<ol>
|
||||||
<li>Choose a sheet name</li>
|
<li>Choose a sheet name</li>
|
||||||
<li>Choose a group: this will impact where/how to archive</li>
|
<li>
|
||||||
|
Choose a group: this will impact where/how to archive
|
||||||
|
</li>
|
||||||
<li>Choose a frequency: how often to archive</li>
|
<li>Choose a frequency: how often to archive</li>
|
||||||
<li>Press "create" and wait</li>
|
<li>Press "create" and wait</li>
|
||||||
<li>Sheet will appear in "Your Sheets" below</li>
|
<li>Sheet will appear in "Your Sheets" below</li>
|
||||||
</ol>
|
</ol>
|
||||||
<small>
|
<small>
|
||||||
<b>NB:</b> This new sheet will be shared with the
|
<b>NB:</b> This new sheet will be shared with the service
|
||||||
service account necessary for Bellingcat's archiving
|
account necessary for Bellingcat's archiving server.
|
||||||
server.
|
|
||||||
</small>
|
</small>
|
||||||
</v-expansion-panel-text>
|
</v-expansion-panel-text>
|
||||||
</v-expansion-panel>
|
</v-expansion-panel>
|
||||||
@@ -40,18 +45,21 @@
|
|||||||
<v-expansion-panel-text>
|
<v-expansion-panel-text>
|
||||||
<ol style="margin-bottom: 1em">
|
<ol style="margin-bottom: 1em">
|
||||||
<li>Choose a group to associate with this Google Sheet</li>
|
<li>Choose a group to associate with this Google Sheet</li>
|
||||||
<li>Invite the provided email as Editor to your Google Sheet</li>
|
|
||||||
<li>
|
<li>
|
||||||
Make sure you have the following <b>mandatory</b> column names:
|
Invite the provided email as Editor to your Google Sheet
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Make sure you have the following <b>mandatory</b> column
|
||||||
|
names:
|
||||||
<ul>
|
<ul>
|
||||||
<li><code>Link</code> where you will put the URLs</li>
|
<li><code>Link</code> where you will put the URLs</li>
|
||||||
<li>
|
<li>
|
||||||
<code>Archive Status</code> to monitor progress and success
|
<code>Archive Status</code> to monitor progress and
|
||||||
of archiver
|
success of archiver
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<code>Archive location</code> where the link to the archived
|
<code>Archive location</code> where the link to the
|
||||||
content is added
|
archived content is added
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
@@ -59,19 +67,23 @@
|
|||||||
Add any of the following <b>optional</b> column names:
|
Add any of the following <b>optional</b> column names:
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<code>Archive date</code> info on when archiving occurred
|
<code>Archive date</code> info on when archiving
|
||||||
|
occurred
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<code>Thumbnail</code> an image preview from archived media
|
<code>Thumbnail</code> an image preview from archived
|
||||||
|
media
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<code>Upload timestamp</code> online content creation date
|
<code>Upload timestamp</code> online content creation
|
||||||
|
date
|
||||||
</li>
|
</li>
|
||||||
<li><code>Upload title</code> title</li>
|
<li><code>Upload title</code> title</li>
|
||||||
<li><code>Textual content</code> text content</li>
|
<li><code>Textual content</code> text content</li>
|
||||||
<li><code>Screenshot</code> link to page screenshot</li>
|
<li><code>Screenshot</code> link to page screenshot</li>
|
||||||
<li>
|
<li>
|
||||||
<code>Hash</code> content hash (for integrity purposes)
|
<code>Hash</code> content hash (for integrity
|
||||||
|
purposes)
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
@@ -79,8 +91,8 @@
|
|||||||
<li>Paste the Google Sheet URL</li>
|
<li>Paste the Google Sheet URL</li>
|
||||||
<li>Press "enable" to add the Google Sheet to your list</li>
|
<li>Press "enable" to add the Google Sheet to your list</li>
|
||||||
<li>
|
<li>
|
||||||
Manually check archiving is working and re-check the steps above
|
Manually check archiving is working and re-check the steps
|
||||||
if it is not
|
above if it is not
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
</v-expansion-panel-text>
|
</v-expansion-panel-text>
|
||||||
@@ -91,7 +103,6 @@
|
|||||||
</v-tabs-window>
|
</v-tabs-window>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-container>
|
</v-container>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -100,12 +111,12 @@ import AddSheet from "@/components/AddSheet.vue";
|
|||||||
export default {
|
export default {
|
||||||
name: "ArchiveSheet",
|
name: "ArchiveSheet",
|
||||||
components: {
|
components: {
|
||||||
AddSheet
|
AddSheet,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
tab: '',
|
tab: "",
|
||||||
items: ['Create new Archiver Sheet', 'Add existing Sheet'],
|
items: ["Create new Archiver Sheet", "Add existing Sheet"],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -114,4 +125,4 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,139 +1,199 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<v-container class="pane-l mb-10">
|
||||||
|
<v-card class="pa-3">
|
||||||
|
<v-card-title class="text-center">
|
||||||
|
Your <u v-if="items">{{ items.length }}</u> active archiver sheets
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
<v-container class="pane-l mb-10">
|
<v-data-table
|
||||||
<v-card class="pa-3">
|
:headers="headers"
|
||||||
|
item-key="name"
|
||||||
|
no-data-text="No Active Sheets available"
|
||||||
|
:items="items"
|
||||||
|
:loading="loading"
|
||||||
|
items-per-page="25"
|
||||||
|
hover
|
||||||
|
>
|
||||||
|
<template v-slot:item.actions="{ item: data }">
|
||||||
|
<v-btn
|
||||||
|
:disabled="!canArchiveNow(data.group_id) || loading"
|
||||||
|
color="teal-lighten-1"
|
||||||
|
size="small"
|
||||||
|
icon
|
||||||
|
class="mx-2"
|
||||||
|
rounded
|
||||||
|
@click="archiveSheetNow(data.id)"
|
||||||
|
><v-icon>mdi-archive-outline</v-icon>
|
||||||
|
|
||||||
<v-card-title class="text-center">
|
<v-tooltip activator="parent" location="left"
|
||||||
Your <u v-if="items">{{ items.length }}</u> active archiver sheets
|
>Archive Now!</v-tooltip
|
||||||
</v-card-title>
|
>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="green-lighten-1"
|
||||||
|
size="small"
|
||||||
|
icon
|
||||||
|
class="mx-2"
|
||||||
|
rounded
|
||||||
|
:href="`https://docs.google.com/spreadsheets/d/${data.id}`"
|
||||||
|
:disabled="loading"
|
||||||
|
target="_blank"
|
||||||
|
><v-icon>mdi-open-in-new</v-icon>
|
||||||
|
<v-tooltip activator="parent" location="left"
|
||||||
|
>Open in new tab</v-tooltip
|
||||||
|
>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="red-lighten-2"
|
||||||
|
size="small"
|
||||||
|
icon
|
||||||
|
class="mx-2"
|
||||||
|
:disabled="loading"
|
||||||
|
rounded
|
||||||
|
@click="removeSheet(data.id)"
|
||||||
|
><v-icon>mdi-stop</v-icon>
|
||||||
|
<v-tooltip activator="parent" location="left"
|
||||||
|
>Stop archiving, does not delete the spreadsheet
|
||||||
|
itself.</v-tooltip
|
||||||
|
>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
<template v-slot:item.name="{ item: data }">
|
||||||
|
<strong :title="data.id">{{ data.name }}</strong>
|
||||||
|
</template>
|
||||||
|
<template v-slot:item.frequency="{ item: data }">
|
||||||
|
<v-chip
|
||||||
|
:color="
|
||||||
|
data.frequency == 'daily' ? 'teal-darken-3' : 'orange-darken-3'
|
||||||
|
"
|
||||||
|
class="bg-white"
|
||||||
|
prepend-icon="mdi-archive-clock-outline"
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
|
{{ data.frequency }}
|
||||||
|
</v-chip>
|
||||||
|
</template>
|
||||||
|
</v-data-table>
|
||||||
|
</v-card>
|
||||||
|
</v-container>
|
||||||
|
|
||||||
<v-data-table :headers="headers" item-key="name" no-data-text="No Active Sheets available" :items="items"
|
<SnackBar
|
||||||
:loading="loading" items-per-page="25" hover>
|
:message="snackbarMessage"
|
||||||
<template v-slot:item.actions="{ item: data }">
|
:show="snackbar"
|
||||||
<v-btn :disabled="!canArchiveNow(data.group_id) || loading" color="teal-lighten-1" size="small" icon class="mx-2" rounded
|
:color="snackbarColor"
|
||||||
@click="archiveSheetNow(data.id)"><v-icon>mdi-archive-outline</v-icon>
|
@update:show="snackbar = $event"
|
||||||
|
/>
|
||||||
<v-tooltip activator="parent" location="left">Archive Now!</v-tooltip>
|
|
||||||
</v-btn>
|
|
||||||
<v-btn color="green-lighten-1" size="small" icon class="mx-2" rounded
|
|
||||||
:href="`https://docs.google.com/spreadsheets/d/${data.id}`" :disabled="loading"
|
|
||||||
target="_blank"><v-icon>mdi-open-in-new</v-icon>
|
|
||||||
<v-tooltip activator="parent" location="left">Open in new tab</v-tooltip>
|
|
||||||
</v-btn>
|
|
||||||
<v-btn color="red-lighten-2" size="small" icon class="mx-2" :disabled="loading" rounded
|
|
||||||
@click="removeSheet(data.id)"><v-icon>mdi-stop</v-icon>
|
|
||||||
<v-tooltip activator="parent" location="left">Stop archiving, does not delete the spreadsheet
|
|
||||||
itself.</v-tooltip>
|
|
||||||
</v-btn>
|
|
||||||
</template>
|
|
||||||
<template v-slot:item.name="{ item: data }">
|
|
||||||
<strong :title="data.id">{{ data.name }}</strong>
|
|
||||||
</template>
|
|
||||||
<template v-slot:item.frequency="{ item: data }">
|
|
||||||
<v-chip :color="data.frequency == 'daily' ? 'teal-darken-3' : 'orange-darken-3'" class="bg-white"
|
|
||||||
prepend-icon="mdi-archive-clock-outline" variant="outlined">
|
|
||||||
{{ data.frequency }}
|
|
||||||
</v-chip>
|
|
||||||
</template>
|
|
||||||
</v-data-table>
|
|
||||||
</v-card>
|
|
||||||
</v-container>
|
|
||||||
|
|
||||||
<SnackBar :message="snackbarMessage" :show="snackbar" :color="snackbarColor" @update:show="snackbar = $event" />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import SnackBar from "@/components/SnackBar.vue";
|
import SnackBar from "@/components/SnackBar.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "ManageSheets",
|
name: "ManageSheets",
|
||||||
components: {
|
components: {
|
||||||
SnackBar,
|
SnackBar,
|
||||||
},
|
},
|
||||||
props: {},
|
props: {},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
snackbar: false,
|
snackbar: false,
|
||||||
snackbarMessage: "",
|
snackbarMessage: "",
|
||||||
snackbarColor: "red",
|
snackbarColor: "red",
|
||||||
|
|
||||||
loading: false,
|
loading: false,
|
||||||
|
|
||||||
headers: [
|
headers: [
|
||||||
{ title: "Name", value: "name", sortable: true },
|
{ title: "Name", value: "name", sortable: true },
|
||||||
{ title: "Group", value: "group_id", sortable: true },
|
{ title: "Group", value: "group_id", sortable: true },
|
||||||
{ title: "Archived", value: "frequency", sortable: true },
|
{ title: "Archived", value: "frequency", sortable: true },
|
||||||
{ title: "Created", value: "created_at", sortable: true },
|
{ title: "Created", value: "created_at", sortable: true },
|
||||||
{ title: "Last archived URL", value: "last_url_archived_at", sortable: true },
|
{
|
||||||
{ title: 'Actions', value: "actions", align: 'center' },
|
title: "Last archived URL",
|
||||||
],
|
value: "last_url_archived_at",
|
||||||
|
sortable: true,
|
||||||
};
|
},
|
||||||
},
|
{ title: "Actions", value: "actions", align: "center" },
|
||||||
computed: {
|
],
|
||||||
user() {
|
};
|
||||||
return this.$store.state.user;
|
},
|
||||||
},
|
computed: {
|
||||||
items() {
|
user() {
|
||||||
return this.$store.state.sheets;
|
return this.$store.state.user;
|
||||||
},
|
},
|
||||||
},
|
items() {
|
||||||
methods: {
|
return this.$store.state.sheets;
|
||||||
showSnackbar(message, color = "red") {
|
},
|
||||||
this.snackbarMessage = message;
|
},
|
||||||
this.snackbarColor = color;
|
methods: {
|
||||||
this.snackbar = true;
|
showSnackbar(message, color = "red") {
|
||||||
},
|
this.snackbarMessage = message;
|
||||||
canArchiveNow(group_id) {
|
this.snackbarColor = color;
|
||||||
return this.$store.state.user?.permissions?.[group_id]?.manually_trigger_sheet || false;
|
this.snackbar = true;
|
||||||
},
|
},
|
||||||
archiveSheetNow(sheetId) {
|
canArchiveNow(group_id) {
|
||||||
this.loading = true;
|
return (
|
||||||
fetch(`${this.$store.state.API_ENDPOINT}/sheet/${sheetId}/archive`, {
|
this.$store.state.user?.permissions?.[group_id]
|
||||||
method: "POST",
|
?.manually_trigger_sheet || false
|
||||||
headers: {
|
);
|
||||||
"Content-Type": "application/json",
|
},
|
||||||
Authorization: `Bearer ${this.$store.state.access_token}`,
|
archiveSheetNow(sheetId) {
|
||||||
}
|
this.loading = true;
|
||||||
}).then(async response => {
|
fetch(`${this.$store.state.API_ENDPOINT}/sheet/${sheetId}/archive`, {
|
||||||
const res = await response.json();
|
method: "POST",
|
||||||
if (response.status === 201) {
|
headers: {
|
||||||
this.showSnackbar(`Sheet ${sheetId} is being archived with task id ${res?.id}!`, "green");
|
"Content-Type": "application/json",
|
||||||
this.$store.dispatch("getSheets");
|
Authorization: `Bearer ${this.$store.state.access_token}`,
|
||||||
} else {
|
},
|
||||||
throw new Error(JSON.stringify(res));
|
})
|
||||||
}
|
.then(async (response) => {
|
||||||
}).catch(error => {
|
const res = await response.json();
|
||||||
console.error("/sheet/mine ", error);
|
if (response.status === 201) {
|
||||||
this.showSnackbar(`Unable to trigger sheet archive: ${error.message}`);
|
this.showSnackbar(
|
||||||
}).finally(() => {
|
`Sheet ${sheetId} is being archived with task id ${res?.id}!`,
|
||||||
this.loading = false;
|
"green"
|
||||||
});
|
);
|
||||||
},
|
this.$store.dispatch("getSheets");
|
||||||
removeSheet(sheetId) {
|
} else {
|
||||||
this.loading = true;
|
throw new Error(JSON.stringify(res));
|
||||||
fetch(`${this.$store.state.API_ENDPOINT}/sheet/${sheetId}`, {
|
}
|
||||||
method: "DELETE",
|
})
|
||||||
headers: {
|
.catch((error) => {
|
||||||
"Content-Type": "application/json",
|
console.error("/sheet/mine ", error);
|
||||||
Authorization: `Bearer ${this.$store.state.access_token}`,
|
this.showSnackbar(
|
||||||
}
|
`Unable to trigger sheet archive: ${error.message}`
|
||||||
}).then(async response => {
|
);
|
||||||
const res = await response.json();
|
})
|
||||||
if (response.status === 200 && res.deleted) {
|
.finally(() => {
|
||||||
this.showSnackbar(`Sheet ${sheetId} has been removed!`, "green");
|
this.loading = false;
|
||||||
this.$store.dispatch("getSheets");
|
});
|
||||||
this.$store.dispatch("checkUserUsage");
|
},
|
||||||
} else {
|
removeSheet(sheetId) {
|
||||||
throw new Error(JSON.stringify(res));
|
this.loading = true;
|
||||||
}
|
fetch(`${this.$store.state.API_ENDPOINT}/sheet/${sheetId}`, {
|
||||||
}).catch(error => {
|
method: "DELETE",
|
||||||
console.error("/sheet/mine ", error);
|
headers: {
|
||||||
this.showSnackbar(`Unable to remove sheet: ${error.message}`);
|
"Content-Type": "application/json",
|
||||||
}).finally(() => {
|
Authorization: `Bearer ${this.$store.state.access_token}`,
|
||||||
this.loading = false;
|
},
|
||||||
});
|
})
|
||||||
},
|
.then(async (response) => {
|
||||||
},
|
const res = await response.json();
|
||||||
|
if (response.status === 200 && res.deleted) {
|
||||||
|
this.showSnackbar(`Sheet ${sheetId} has been removed!`, "green");
|
||||||
|
this.$store.dispatch("getSheets");
|
||||||
|
this.$store.dispatch("checkUserUsage");
|
||||||
|
} else {
|
||||||
|
throw new Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("/sheet/mine ", error);
|
||||||
|
this.showSnackbar(`Unable to remove sheet: ${error.message}`);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -6,38 +6,75 @@
|
|||||||
</router-link>
|
</router-link>
|
||||||
</v-toolbar-title>
|
</v-toolbar-title>
|
||||||
|
|
||||||
|
<v-chip
|
||||||
<v-chip v-if="$store.state.errorMessage" :title="$store.state.errorMessage" color="red" variant="tonal" closable
|
v-if="$store.state.errorMessage"
|
||||||
class="mx-4">
|
:title="$store.state.errorMessage"
|
||||||
|
color="red"
|
||||||
|
variant="tonal"
|
||||||
|
closable
|
||||||
|
class="mx-4"
|
||||||
|
>
|
||||||
ERROR: {{ $store.state.errorMessage }}
|
ERROR: {{ $store.state.errorMessage }}
|
||||||
</v-chip>
|
</v-chip>
|
||||||
|
|
||||||
<v-spacer v-if="!smAndDown"></v-spacer>
|
<v-spacer v-if="!smAndDown"></v-spacer>
|
||||||
<div v-if="user?.active && !smAndDown">
|
<div v-if="user?.active && !smAndDown">
|
||||||
<template v-for="btn in btns">
|
<template v-for="btn in btns">
|
||||||
<v-btn :to="btn.to" :prepend-icon="btn.icon" variant="text" class="nodecoration ml-2" size="large" active-color="teal">
|
<v-btn
|
||||||
|
:to="btn.to"
|
||||||
|
:prepend-icon="btn.icon"
|
||||||
|
variant="text"
|
||||||
|
class="nodecoration ml-2"
|
||||||
|
size="large"
|
||||||
|
active-color="teal"
|
||||||
|
>
|
||||||
{{ btn.text }}
|
{{ btn.text }}
|
||||||
<v-tooltip activator="parent" location="bottom">{{ btn.tooltip }}</v-tooltip>
|
<v-tooltip activator="parent" location="bottom">{{
|
||||||
|
btn.tooltip
|
||||||
|
}}</v-tooltip>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<v-spacer v-if="!smAndDown"></v-spacer>
|
<v-spacer v-if="!smAndDown"></v-spacer>
|
||||||
<span class="user mx-2 pa-2 bg-blue-grey-lighten-5 rounded elevation-2" v-if="user">
|
<span
|
||||||
|
class="user mx-2 pa-2 bg-blue-grey-lighten-5 rounded elevation-2"
|
||||||
|
v-if="user"
|
||||||
|
>
|
||||||
<span v-if="!loadingUserState">
|
<span v-if="!loadingUserState">
|
||||||
<v-chip v-if="user.active" color="green" class="bg-white" prepend-icon="mdi-checkbox-marked-circle"
|
<v-chip
|
||||||
variant="outlined">
|
v-if="user.active"
|
||||||
|
color="green"
|
||||||
|
class="bg-white"
|
||||||
|
prepend-icon="mdi-checkbox-marked-circle"
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
active
|
active
|
||||||
</v-chip>
|
</v-chip>
|
||||||
<v-chip v-if="!user.active" color="red" class="bg-white" prepend-icon="mdi-account-cancel" variant="outlined">
|
<v-chip
|
||||||
|
v-if="!user.active"
|
||||||
|
color="red"
|
||||||
|
class="bg-white"
|
||||||
|
prepend-icon="mdi-account-cancel"
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
inactive
|
inactive
|
||||||
</v-chip>
|
</v-chip>
|
||||||
|
|
||||||
<v-tooltip activator="parent" location="bottom">{{ activeUserMessage }}</v-tooltip>
|
<v-tooltip activator="parent" location="bottom">{{
|
||||||
|
activeUserMessage
|
||||||
|
}}</v-tooltip>
|
||||||
</span>
|
</span>
|
||||||
<span class="ms-2">{{ user.email }}</span>
|
<span class="ms-2">{{ user.email }}</span>
|
||||||
<v-btn v-if="!smAndDown" prepend-icon="mdi-logout" variant="text" class="mx-2 elevation-2 bg-white" size="small"
|
<v-btn
|
||||||
@click="$store.dispatch('signout')">Sign Out</v-btn>
|
v-if="!smAndDown"
|
||||||
|
prepend-icon="mdi-logout"
|
||||||
|
variant="text"
|
||||||
|
class="mx-2 elevation-2 bg-white"
|
||||||
|
size="small"
|
||||||
|
@click="$store.dispatch('signout')"
|
||||||
|
>Sign Out</v-btn
|
||||||
|
>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<v-btn v-if="!user" @click="$store.dispatch('signin')">Sign In</v-btn>
|
<v-btn v-if="!user" @click="$store.dispatch('signin')">Sign In</v-btn>
|
||||||
@@ -48,21 +85,33 @@
|
|||||||
</template>
|
</template>
|
||||||
<v-list>
|
<v-list>
|
||||||
<v-list-item v-for="btn in btns" :key="btn.to" :to="btn.to">
|
<v-list-item v-for="btn in btns" :key="btn.to" :to="btn.to">
|
||||||
<v-btn :prepend-icon="btn.icon" variant="plain" class="nodecoration" size="large">
|
<v-btn
|
||||||
|
:prepend-icon="btn.icon"
|
||||||
|
variant="plain"
|
||||||
|
class="nodecoration"
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
{{ btn.text }}
|
{{ btn.text }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-tooltip activator="parent" location="left">{{ btn.tooltip }}</v-tooltip>
|
<v-tooltip activator="parent" location="left">{{
|
||||||
|
btn.tooltip
|
||||||
|
}}</v-tooltip>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
<v-list-item @click="$store.dispatch('signout')">
|
<v-list-item @click="$store.dispatch('signout')">
|
||||||
<v-btn prepend-icon="mdi-logout" variant="plain" class="nodecoration" size="large">Sign Out</v-btn>
|
<v-btn
|
||||||
|
prepend-icon="mdi-logout"
|
||||||
|
variant="plain"
|
||||||
|
class="nodecoration"
|
||||||
|
size="large"
|
||||||
|
>Sign Out</v-btn
|
||||||
|
>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-menu>
|
</v-menu>
|
||||||
</v-app-bar>
|
</v-app-bar>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from "vuetify";
|
||||||
const { smAndDown } = useDisplay();
|
const { smAndDown } = useDisplay();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -73,10 +122,25 @@ export default {
|
|||||||
return {
|
return {
|
||||||
drawer: false,
|
drawer: false,
|
||||||
btns: [
|
btns: [
|
||||||
{ to: "/", icon: "mdi-table-large", text: "Sheets", tooltip: "Create, manage, and archive Google Sheets." },
|
{
|
||||||
{ to: "/url", icon: "mdi-cloud-download-outline", text: "URL", tooltip: "Archive a single URL." },
|
to: "/",
|
||||||
{ to: "/archives", icon: "mdi-magnify", text: "Archives", tooltip: "Search for archived URLs." },
|
icon: "mdi-table-large",
|
||||||
]
|
text: "Sheets",
|
||||||
|
tooltip: "Create, manage, and archive Google Sheets.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
to: "/url",
|
||||||
|
icon: "mdi-cloud-download-outline",
|
||||||
|
text: "URL",
|
||||||
|
tooltip: "Archive a single URL.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
to: "/archives",
|
||||||
|
icon: "mdi-magnify",
|
||||||
|
text: "Archives",
|
||||||
|
tooltip: "Search for archived URLs.",
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -89,9 +153,9 @@ export default {
|
|||||||
}
|
}
|
||||||
return "This account is inactive, please reach out to the Bellingcat team for access.";
|
return "This account is inactive, please reach out to the Bellingcat team for access.";
|
||||||
},
|
},
|
||||||
loadingUserState() {
|
loadingUserState() {
|
||||||
return this.$store.state?.loadingUserState;
|
return this.$store.state?.loadingUserState;
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,32 +1,39 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-alert v-if="!loadingUserState" color="orange" icon="mdi-information" class="text-center"
|
<v-alert
|
||||||
style="font-size:x-large">
|
v-if="!loadingUserState"
|
||||||
To use the <strong>{{ feature }}</strong> feature, you need <strong>permission from Bellingcat's tech
|
color="orange"
|
||||||
team</strong>.
|
icon="mdi-information"
|
||||||
<br />
|
class="text-center"
|
||||||
You can ask for access via <a href="https://forms.gle/crqBXUtyZcbLhiRQ9" target="_blank">this form</a>.
|
style="font-size: x-large"
|
||||||
<br />
|
>
|
||||||
<small>
|
To use the <strong>{{ feature }}</strong> feature, you need
|
||||||
<strong>NB: </strong>We do not allow law enforcement, military or intelligence agencies to use this tool.
|
<strong>permission from Bellingcat's tech team</strong>.
|
||||||
</small>
|
<br />
|
||||||
</v-alert>
|
You can ask for access via
|
||||||
|
<a href="https://forms.gle/crqBXUtyZcbLhiRQ9" target="_blank">this form</a>.
|
||||||
|
<br />
|
||||||
|
<small>
|
||||||
|
<strong>NB: </strong>We do not allow law enforcement, military or
|
||||||
|
intelligence agencies to use this tool.
|
||||||
|
</small>
|
||||||
|
</v-alert>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: 'PermissionNeeded',
|
name: "PermissionNeeded",
|
||||||
props: {
|
props: {
|
||||||
feature: {
|
feature: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false
|
required: false,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
loadingUserState() {
|
loadingUserState() {
|
||||||
return this.$store.state?.loadingUserState;
|
return this.$store.state?.loadingUserState;
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
@@ -1,57 +1,63 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-snackbar v-model="visible" :timeout="timeout" :top="top" :bottom="bottom" close-on-content-click>
|
<v-snackbar
|
||||||
{{ message }}
|
v-model="visible"
|
||||||
<template v-slot:actions>
|
:timeout="timeout"
|
||||||
<v-btn :color="color" variant="text" @click="visible = false">
|
:top="top"
|
||||||
Close
|
:bottom="bottom"
|
||||||
</v-btn>
|
close-on-content-click
|
||||||
</template>
|
>
|
||||||
</v-snackbar>
|
{{ message }}
|
||||||
|
<template v-slot:actions>
|
||||||
|
<v-btn :color="color" variant="text" @click="visible = false">
|
||||||
|
Close
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
</v-snackbar>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: 'MySnackBar',
|
name: "MySnackBar",
|
||||||
props: {
|
props: {
|
||||||
message: {
|
message: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
timeout: {
|
timeout: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 5000
|
default: 5000,
|
||||||
},
|
},
|
||||||
color: {
|
color: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'orange'
|
default: "orange",
|
||||||
},
|
},
|
||||||
top: {
|
top: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false,
|
||||||
},
|
},
|
||||||
bottom: {
|
bottom: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true,
|
||||||
},
|
},
|
||||||
show: {
|
show: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true
|
required: true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
visible: this.show
|
visible: this.show,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
show(val) {
|
show(val) {
|
||||||
this.visible = val;
|
this.visible = val;
|
||||||
},
|
},
|
||||||
visible(val) {
|
visible(val) {
|
||||||
if (!val) {
|
if (!val) {
|
||||||
this.$emit('update:show', false);
|
this.$emit("update:show", false);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,57 +1,67 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-container class="pane" fluid v-if="!user || !user.active">
|
<v-container class="pane" fluid v-if="!user || !user.active">
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col>
|
<v-col>
|
||||||
<v-card>
|
<v-card>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-card-title class="text-center">
|
<v-card-title class="text-center">
|
||||||
Welcome to the Auto Archiver Setup Tool
|
Welcome to the Auto Archiver Setup Tool
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
<v-alert color="#f2d97c" icon="mdi-alert">
|
<v-alert color="#f2d97c" icon="mdi-alert">
|
||||||
This is a prototype demo service provided on a
|
This is a prototype demo service provided on a best-effort basis.
|
||||||
best-effort basis. <br />Do not use for mission critical or sensitive
|
<br />Do not use for mission critical or sensitive data.
|
||||||
data.
|
</v-alert>
|
||||||
</v-alert>
|
<p>
|
||||||
<p>
|
This tool can be used to archive digital content via single URL or
|
||||||
This tool can be used to archive digital content via single URL or Google Sheets, you can
|
Google Sheets, you can also search for archived content.
|
||||||
also search for
|
</p>
|
||||||
archived content.
|
<div class="text-center">
|
||||||
</p>
|
<v-btn
|
||||||
<div class="text-center">
|
v-if="!user && !loadingUserState"
|
||||||
<v-btn v-if="!user && !loadingUserState" @click="$store.dispatch('signin')" size="large">Sign In</v-btn>
|
@click="$store.dispatch('signin')"
|
||||||
</div>
|
size="large"
|
||||||
<v-container v-if="loadingUserState" class="pane" style="text-align: center;">
|
>Sign In</v-btn
|
||||||
<v-row justify="center">
|
>
|
||||||
<v-col cols="12">
|
</div>
|
||||||
<v-progress-circular color="teal" indeterminate :size="82"
|
<v-container
|
||||||
:width="7"></v-progress-circular>
|
v-if="loadingUserState"
|
||||||
</v-col>
|
class="pane"
|
||||||
<v-col cols="12">
|
style="text-align: center"
|
||||||
<h4>loading...</h4>
|
>
|
||||||
</v-col>
|
<v-row justify="center">
|
||||||
</v-row>
|
<v-col cols="12">
|
||||||
</v-container>
|
<v-progress-circular
|
||||||
</v-card-text>
|
color="teal"
|
||||||
</v-card>
|
indeterminate
|
||||||
</v-col>
|
:size="82"
|
||||||
</v-row>
|
:width="7"
|
||||||
</v-container>
|
></v-progress-circular>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12">
|
||||||
|
<h4>loading...</h4>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: 'WelcomeCard',
|
name: "WelcomeCard",
|
||||||
props: {
|
props: {},
|
||||||
},
|
computed: {
|
||||||
computed: {
|
user() {
|
||||||
user() {
|
return this.$store.state.user;
|
||||||
return this.$store.state.user;
|
},
|
||||||
},
|
loadingUserState() {
|
||||||
loadingUserState() {
|
return this.$store.state?.loadingUserState;
|
||||||
return this.$store.state?.loadingUserState;
|
},
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { createApp } from "vue";
|
import { createApp } from "vue";
|
||||||
import { createVuetify } from "vuetify";
|
import { createVuetify } from "vuetify";
|
||||||
import * as components from 'vuetify/components';
|
import * as components from "vuetify/components";
|
||||||
import * as directives from 'vuetify/directives';
|
import * as directives from "vuetify/directives";
|
||||||
import { VDateInput } from 'vuetify/labs/VDateInput';
|
import { VDateInput } from "vuetify/labs/VDateInput";
|
||||||
import App from "./App.vue";
|
import App from "./App.vue";
|
||||||
import router from "./router";
|
import router from "./router";
|
||||||
import store from "./store";
|
import store from "./store";
|
||||||
@@ -12,7 +12,7 @@ import "@mdi/font/css/materialdesignicons.css";
|
|||||||
import "./styles/global.css";
|
import "./styles/global.css";
|
||||||
|
|
||||||
const vuetify = createVuetify({
|
const vuetify = createVuetify({
|
||||||
components: { ...components, VDateInput, },
|
components: { ...components, VDateInput },
|
||||||
directives,
|
directives,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,41 +1,41 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router';
|
import { createRouter, createWebHistory } from "vue-router";
|
||||||
import HomeView from '../views/HomeView.vue';
|
import HomeView from "../views/HomeView.vue";
|
||||||
import ArchiveSearchView from '../views/ArchiveSearchView.vue';
|
import ArchiveSearchView from "../views/ArchiveSearchView.vue";
|
||||||
|
|
||||||
import ArchiveUrlView from "../views/ArchiveUrlView.vue";
|
import ArchiveUrlView from "../views/ArchiveUrlView.vue";
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: "/",
|
||||||
name: 'home',
|
name: "home",
|
||||||
component: HomeView,
|
component: HomeView,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/url',
|
path: "/url",
|
||||||
name: 'URL Archiving',
|
name: "URL Archiving",
|
||||||
component: ArchiveUrlView,
|
component: ArchiveUrlView,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/archives',
|
path: "/archives",
|
||||||
name: 'Archives search',
|
name: "Archives search",
|
||||||
component: ArchiveSearchView,
|
component: ArchiveSearchView,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/privacy',
|
path: "/privacy",
|
||||||
name: 'Privacy Policy',
|
name: "Privacy Policy",
|
||||||
component: () =>
|
component: () =>
|
||||||
import(/* webpackChunkName: "privacy" */ '../views/PrivacyView.vue'),
|
import(/* webpackChunkName: "privacy" */ "../views/PrivacyView.vue"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/tos',
|
path: "/tos",
|
||||||
name: 'Terms of Use',
|
name: "Terms of Use",
|
||||||
component: () =>
|
component: () =>
|
||||||
import(/* webpackChunkName: "tos" */ '../views/TOSView.vue'),
|
import(/* webpackChunkName: "tos" */ "../views/TOSView.vue"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/:pathMatch(.*)*',
|
path: "/:pathMatch(.*)*",
|
||||||
redirect: '/',
|
redirect: "/",
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { createStore } from "vuex";
|
import { createStore } from "vuex";
|
||||||
import { gapi, client } from "@/gapi";
|
import { gapi } from "@/gapi";
|
||||||
import {
|
import {
|
||||||
signOut,
|
signOut,
|
||||||
GoogleAuthProvider,
|
GoogleAuthProvider,
|
||||||
signInWithCredential,
|
signInWithCredential,
|
||||||
|
browserLocalPersistence,
|
||||||
|
setPersistence,
|
||||||
} from "firebase/auth";
|
} from "firebase/auth";
|
||||||
import { collection, } from "firebase/firestore";
|
import { firebaseAuth } from "@/firebase.js";
|
||||||
import { firebaseAuth, firebaseFirestore } from "@/firebase.js";
|
|
||||||
|
|
||||||
function saveToLocalStorage(state) {
|
function saveToLocalStorage(state) {
|
||||||
localStorage.setItem("user", JSON.stringify(state.user));
|
localStorage.setItem("user", JSON.stringify(state.user));
|
||||||
@@ -25,7 +26,7 @@ function clearLocalStorage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function waitForGapiAuth2() {
|
async function waitForGapiAuth2() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, _reject) => {
|
||||||
const checkGapiAuth2 = () => {
|
const checkGapiAuth2 = () => {
|
||||||
if (gapi.auth2 && gapi.auth2.getAuthInstance()) {
|
if (gapi.auth2 && gapi.auth2.getAuthInstance()) {
|
||||||
resolve(gapi.auth2.getAuthInstance());
|
resolve(gapi.auth2.getAuthInstance());
|
||||||
@@ -45,9 +46,8 @@ export default createStore({
|
|||||||
sheets: [],
|
sheets: [],
|
||||||
loadingUserState: false,
|
loadingUserState: false,
|
||||||
errorMessage: "",
|
errorMessage: "",
|
||||||
// # TODO: reenable production API endpoint
|
|
||||||
// API_ENDPOINT: "https://auto-archiver-api.bellingcat.com"
|
// API_ENDPOINT: "https://auto-archiver-api.bellingcat.com"
|
||||||
API_ENDPOINT: "http://localhost:8004"
|
API_ENDPOINT: process.env.VUE_APP_API_ENDPOINT || "http://localhost:8004",
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
setUser(state, user) {
|
setUser(state, user) {
|
||||||
@@ -60,7 +60,9 @@ export default createStore({
|
|||||||
},
|
},
|
||||||
setUserPermissions(state, permissions) {
|
setUserPermissions(state, permissions) {
|
||||||
state.user.permissions = permissions;
|
state.user.permissions = permissions;
|
||||||
state.user.groups = Object.keys(permissions).filter(key => key !== "all");
|
state.user.groups = Object.keys(permissions).filter(
|
||||||
|
(key) => key !== "all"
|
||||||
|
);
|
||||||
state.loadingUserState = false;
|
state.loadingUserState = false;
|
||||||
saveToLocalStorage(state);
|
saveToLocalStorage(state);
|
||||||
},
|
},
|
||||||
@@ -90,6 +92,10 @@ export default createStore({
|
|||||||
commit("setAccessToken", access_token);
|
commit("setAccessToken", access_token);
|
||||||
const credential = GoogleAuthProvider.credential(null, access_token);
|
const credential = GoogleAuthProvider.credential(null, access_token);
|
||||||
|
|
||||||
|
// Set persistence before signing in
|
||||||
|
await setPersistence(firebaseAuth, browserLocalPersistence);
|
||||||
|
|
||||||
|
// Sign in with the provided credential
|
||||||
const response = await signInWithCredential(firebaseAuth, credential);
|
const response = await signInWithCredential(firebaseAuth, credential);
|
||||||
|
|
||||||
commit("setUser", response.user);
|
commit("setUser", response.user);
|
||||||
@@ -108,7 +114,7 @@ export default createStore({
|
|||||||
callback,
|
callback,
|
||||||
});
|
});
|
||||||
|
|
||||||
client.requestAccessToken();
|
await client.requestAccessToken();
|
||||||
},
|
},
|
||||||
|
|
||||||
async signout({ commit }) {
|
async signout({ commit }) {
|
||||||
@@ -138,16 +144,14 @@ export default createStore({
|
|||||||
async checkActiveUser({ state, dispatch, commit }) {
|
async checkActiveUser({ state, dispatch, commit }) {
|
||||||
try {
|
try {
|
||||||
commit("setErrorMessage", "");
|
commit("setErrorMessage", "");
|
||||||
const r = await fetch(
|
console.log(`${state.API_ENDPOINT}/user/active`);
|
||||||
`${state.API_ENDPOINT}/user/active`,
|
const r = await fetch(`${state.API_ENDPOINT}/user/active`, {
|
||||||
{
|
method: "GET",
|
||||||
method: "GET",
|
headers: {
|
||||||
headers: {
|
"Content-Type": "application/json",
|
||||||
"Content-Type": "application/json",
|
Authorization: `Bearer ${state.access_token}`,
|
||||||
Authorization: `Bearer ${state.access_token}`,
|
},
|
||||||
},
|
});
|
||||||
}
|
|
||||||
)
|
|
||||||
const response = await r.json();
|
const response = await r.json();
|
||||||
commit("setUserActiveState", response.active);
|
commit("setUserActiveState", response.active);
|
||||||
if (response.active === true) {
|
if (response.active === true) {
|
||||||
@@ -155,48 +159,51 @@ export default createStore({
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("checkActiveUser (firebase.js): ", error);
|
console.error("checkActiveUser (firebase.js): ", error);
|
||||||
commit("setErrorMessage", "Unable to check user status against the API");
|
commit(
|
||||||
|
"setErrorMessage",
|
||||||
|
"Unable to check user status against the API"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async checkUserPermissions({ state, commit }) {
|
async checkUserPermissions({ state, commit }) {
|
||||||
try {
|
try {
|
||||||
commit("setErrorMessage", "");
|
commit("setErrorMessage", "");
|
||||||
const r = await fetch(
|
const r = await fetch(`${state.API_ENDPOINT}/user/permissions`, {
|
||||||
`${state.API_ENDPOINT}/user/permissions`,
|
method: "GET",
|
||||||
{
|
headers: {
|
||||||
method: "GET",
|
"Content-Type": "application/json",
|
||||||
headers: {
|
Authorization: `Bearer ${state.access_token}`,
|
||||||
"Content-Type": "application/json",
|
},
|
||||||
Authorization: `Bearer ${state.access_token}`,
|
});
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const response = await r.json();
|
const response = await r.json();
|
||||||
commit("setUserPermissions", response);
|
commit("setUserPermissions", response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("checkUserPermissions (firebase.js): ", error);
|
console.error("checkUserPermissions (firebase.js): ", error);
|
||||||
commit("setErrorMessage", "Unable to fetch user permissions from the API");
|
commit(
|
||||||
|
"setErrorMessage",
|
||||||
|
"Unable to fetch user permissions from the API"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async checkUserUsage({ state, commit }) {
|
async checkUserUsage({ state, commit }) {
|
||||||
try {
|
try {
|
||||||
commit("setErrorMessage", "");
|
commit("setErrorMessage", "");
|
||||||
const r = await fetch(
|
const r = await fetch(`${state.API_ENDPOINT}/user/usage`, {
|
||||||
`${state.API_ENDPOINT}/user/usage`,
|
method: "GET",
|
||||||
{
|
headers: {
|
||||||
method: "GET",
|
"Content-Type": "application/json",
|
||||||
headers: {
|
Authorization: `Bearer ${state.access_token}`,
|
||||||
"Content-Type": "application/json",
|
},
|
||||||
Authorization: `Bearer ${state.access_token}`,
|
});
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const response = await r.json();
|
const response = await r.json();
|
||||||
commit("setUserUsage", response);
|
commit("setUserUsage", response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("checkUserUsage (firebase.js): ", error);
|
console.error("checkUserUsage (firebase.js): ", error);
|
||||||
commit("setErrorMessage", "Unable to fetch user usage quota from the API");
|
commit(
|
||||||
|
"setErrorMessage",
|
||||||
|
"Unable to fetch user usage quota from the API"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -210,8 +217,8 @@ export default createStore({
|
|||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: `Bearer ${state.access_token}`,
|
Authorization: `Bearer ${state.access_token}`,
|
||||||
}
|
},
|
||||||
}).then(async response => {
|
}).then(async (response) => {
|
||||||
const res = await response.json();
|
const res = await response.json();
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
commit("setSheets", res);
|
commit("setSheets", res);
|
||||||
@@ -219,13 +226,14 @@ export default createStore({
|
|||||||
throw new Error(JSON.stringify(res));
|
throw new Error(JSON.stringify(res));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("getSheets (firebase.js): ", error);
|
console.error("getSheets (firebase.js): ", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
async createSheet({ state, dispatch, commit }, {name, service_account_email}) {
|
async createSheet(
|
||||||
|
{ _state, dispatch, _commit },
|
||||||
|
{ name, service_account_email }
|
||||||
|
) {
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
// create new sheet
|
// create new sheet
|
||||||
@@ -354,8 +362,7 @@ export default createStore({
|
|||||||
resource: {
|
resource: {
|
||||||
role: "writer",
|
role: "writer",
|
||||||
type: "user",
|
type: "user",
|
||||||
emailAddress:
|
emailAddress: service_account_email,
|
||||||
service_account_email,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -374,7 +381,9 @@ export default createStore({
|
|||||||
isTokenExpired: async (state) => {
|
isTokenExpired: async (state) => {
|
||||||
if (!state.access_token) return true;
|
if (!state.access_token) return true;
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`https://oauth2.googleapis.com/tokeninfo?access_token=${state.access_token}`);
|
const response = await fetch(
|
||||||
|
`https://oauth2.googleapis.com/tokeninfo?access_token=${state.access_token}`
|
||||||
|
);
|
||||||
if (response.status !== 200) return true;
|
if (response.status !== 200) return true;
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.expires_in > 0) return false;
|
if (data.expires_in > 0) return false;
|
||||||
@@ -398,18 +407,20 @@ export default createStore({
|
|||||||
store.commit("setLoadingUserState", true);
|
store.commit("setLoadingUserState", true);
|
||||||
store.commit("setUser", user);
|
store.commit("setUser", user);
|
||||||
store.commit("setAccessToken", access_token);
|
store.commit("setAccessToken", access_token);
|
||||||
store.getters.isTokenExpired.then((expired) => {
|
store.getters.isTokenExpired
|
||||||
if (expired) {
|
.then((expired) => {
|
||||||
|
if (expired) {
|
||||||
|
store.dispatch("signout");
|
||||||
|
} else {
|
||||||
|
store.dispatch("checkActiveUser");
|
||||||
|
store.dispatch("checkUserPermissions");
|
||||||
|
store.dispatch("checkUserUsage");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error checking token expiration:", error);
|
||||||
store.dispatch("signout");
|
store.dispatch("signout");
|
||||||
} else {
|
});
|
||||||
store.dispatch("checkActiveUser");
|
|
||||||
store.dispatch("checkUserPermissions");
|
|
||||||
store.dispatch("checkUserUsage");
|
|
||||||
}
|
|
||||||
}).catch((error) => {
|
|
||||||
console.error("Error checking token expiration:", error);
|
|
||||||
store.dispatch("signout");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
|
|
||||||
|
|
||||||
export function urlValidator(url) {
|
export function urlValidator(url) {
|
||||||
if (!url) return true;
|
if (!url) return true;
|
||||||
if (url.length < 10) return "URL is too short";
|
if (url.length < 10) return "URL is too short";
|
||||||
try {
|
try {
|
||||||
new URL(url);
|
new URL(url);
|
||||||
return true;
|
return true;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return "Not a valid URL";
|
return "Not a valid URL";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getUrlFromResult(item) {
|
export function getUrlFromResult(item) {
|
||||||
const final_media = item.result?.media?.filter(m => m?.properties?.id == '_final_media');
|
const final_media = item.result?.media?.filter(
|
||||||
if (final_media && final_media.length > 0) {
|
(m) => m?.properties?.id == "_final_media"
|
||||||
return final_media[0].urls;
|
);
|
||||||
}
|
if (final_media && final_media.length > 0) {
|
||||||
};
|
return final_media[0].urls;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<PermissionNeeded v-if="user && !featureEnabled" feature="Search Archives" />
|
<PermissionNeeded v-if="user && !featureEnabled" feature="Search Archives" />
|
||||||
<WelcomeCard/>
|
<WelcomeCard />
|
||||||
<v-container class="pane-l" v-if="user?.active && featureEnabled">
|
<v-container class="pane-l" v-if="user?.active && featureEnabled">
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col>
|
<v-col>
|
||||||
@@ -12,19 +12,43 @@
|
|||||||
<v-form>
|
<v-form>
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col cols="12" md="6">
|
<v-col cols="12" md="6">
|
||||||
<v-date-input v-model="queryAfter" label="Archived After" variant="outlined" min="2022-01-01"
|
<v-date-input
|
||||||
:max="queryBefore || today"></v-date-input>
|
v-model="queryAfter"
|
||||||
|
label="Archived After"
|
||||||
|
variant="outlined"
|
||||||
|
min="2022-01-01"
|
||||||
|
:max="queryBefore || today"
|
||||||
|
></v-date-input>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="6">
|
<v-col cols="12" md="6">
|
||||||
<v-date-input v-model="queryBefore" label="Archived Before" variant="outlined"
|
<v-date-input
|
||||||
:min="queryAfter || '2022-01-01'" :max="today"></v-date-input>
|
v-model="queryBefore"
|
||||||
|
label="Archived Before"
|
||||||
|
variant="outlined"
|
||||||
|
:min="queryAfter || '2022-01-01'"
|
||||||
|
:max="today"
|
||||||
|
></v-date-input>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
<v-text-field ref="searchInput" v-model="queryUrl" label="Search for this URL" prepend-icon="mdi-web"
|
<v-text-field
|
||||||
variant="outlined" :rules="[urlValidator]" required @keyup.enter="searchForArchives"></v-text-field>
|
ref="searchInput"
|
||||||
|
v-model="queryUrl"
|
||||||
|
label="Search for this URL"
|
||||||
|
prepend-icon="mdi-web"
|
||||||
|
variant="outlined"
|
||||||
|
:rules="[urlValidator]"
|
||||||
|
required
|
||||||
|
@keyup.enter="searchForArchives"
|
||||||
|
></v-text-field>
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col cols="12" class="text-right">
|
<v-col cols="12" class="text-right">
|
||||||
<v-btn @click="searchForArchives" color="teal" class="mt-4" size="large" :disabled="!validUrl">
|
<v-btn
|
||||||
|
@click="searchForArchives"
|
||||||
|
color="teal"
|
||||||
|
class="mt-4"
|
||||||
|
size="large"
|
||||||
|
:disabled="!validUrl"
|
||||||
|
>
|
||||||
Search
|
Search
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-col>
|
</v-col>
|
||||||
@@ -32,39 +56,93 @@
|
|||||||
</v-form>
|
</v-form>
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col>
|
<v-col>
|
||||||
<v-snackbar v-model="snackbar" :timeout="4000" top right close-on-content-click>
|
<v-snackbar
|
||||||
|
v-model="snackbar"
|
||||||
|
:timeout="4000"
|
||||||
|
top
|
||||||
|
right
|
||||||
|
close-on-content-click
|
||||||
|
>
|
||||||
{{ snackbarMessage }}
|
{{ snackbarMessage }}
|
||||||
<template v-slot:actions>
|
<template v-slot:actions>
|
||||||
<v-btn color="orange" variant="text" @click="snackbar = false">
|
<v-btn
|
||||||
|
color="orange"
|
||||||
|
variant="text"
|
||||||
|
@click="snackbar = false"
|
||||||
|
>
|
||||||
Close
|
Close
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
</v-snackbar>
|
</v-snackbar>
|
||||||
<v-data-table-server density="compact" loading-text="Loading... Please wait"
|
<v-data-table-server
|
||||||
no-data-text="Nothing found" v-model:items-per-page="itemsPerPage" :headers="headers"
|
density="compact"
|
||||||
:items="serverItems" :items-length="totalItems" :loading="loading" :search="tableSearch"
|
loading-text="Loading... Please wait"
|
||||||
@update:options="loadItems" :items-per-page-options="pageOptions" show-expand item-value="id"
|
no-data-text="Nothing found"
|
||||||
fixed-header>
|
v-model:items-per-page="itemsPerPage"
|
||||||
|
:headers="headers"
|
||||||
|
:items="serverItems"
|
||||||
|
:items-length="totalItems"
|
||||||
|
:loading="loading"
|
||||||
|
:search="tableSearch"
|
||||||
|
@update:options="loadItems"
|
||||||
|
:items-per-page-options="pageOptions"
|
||||||
|
show-expand
|
||||||
|
item-value="id"
|
||||||
|
fixed-header
|
||||||
|
>
|
||||||
<template v-slot:item.result="{ item }">
|
<template v-slot:item.result="{ item }">
|
||||||
<a :href="getUrlFromResult(item)" target="_blank" rel="noopener noreferrer">{{ item.result?.status
|
<a
|
||||||
}}</a>
|
:href="getUrlFromResult(item)"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>{{ item.result?.status }}</a
|
||||||
|
>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:item.url="{ item }">
|
<template v-slot:item.url="{ item }">
|
||||||
<a :href="item.url" target="_blank" rel="noopener noreferrer">{{ item.url }}</a>
|
<a
|
||||||
|
:href="item.url"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>{{ item.url }}</a
|
||||||
|
>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:item.created_at="{ item }">
|
<template v-slot:item.created_at="{ item }">
|
||||||
<time :datetime="item?.created_at"
|
<time
|
||||||
:title="$moment(item?.created_at).format(`MMMM Do YYYY, k:mm:ss`)">{{
|
:datetime="item?.created_at"
|
||||||
$moment(item?.created_at).fromNow() }}</time>
|
:title="
|
||||||
|
$moment(item?.created_at).format(
|
||||||
|
`MMMM Do YYYY, k:mm:ss`
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>{{ $moment(item?.created_at).fromNow() }}</time
|
||||||
|
>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:item.store_until="{ item }">
|
<template v-slot:item.store_until="{ item }">
|
||||||
<time :datetime="item?.store_until"
|
<time
|
||||||
:title="`this archive will be deleted on: ${$moment(item?.store_until).format(`MMMM Do YYYY, k:mm:ss`)}`"
|
:datetime="item?.store_until"
|
||||||
:style="{ color: $moment().diff(item?.store_until, 'days') > -31 ? 'red' : 'inherit' }">{{
|
:title="`this archive will be deleted on: ${$moment(
|
||||||
item?.store_until ? $moment(item?.store_until).fromNow() : "never" }}</time>
|
item?.store_until
|
||||||
|
).format(`MMMM Do YYYY, k:mm:ss`)}`"
|
||||||
|
:style="{
|
||||||
|
color:
|
||||||
|
$moment().diff(item?.store_until, 'days') > -31
|
||||||
|
? 'red'
|
||||||
|
: 'inherit',
|
||||||
|
}"
|
||||||
|
>{{
|
||||||
|
item?.store_until
|
||||||
|
? $moment(item?.store_until).fromNow()
|
||||||
|
: "never"
|
||||||
|
}}</time
|
||||||
|
>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:item.size="{ item }">
|
<template v-slot:item.size="{ item }">
|
||||||
{{ ((item?.result?.metadata?.total_bytes || 0) / (1024 * 1024)).toFixed(2) }}
|
{{
|
||||||
|
(
|
||||||
|
(item?.result?.metadata?.total_bytes || 0) /
|
||||||
|
(1024 * 1024)
|
||||||
|
).toFixed(2)
|
||||||
|
}}
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:item.files="{ item }">
|
<template v-slot:item.files="{ item }">
|
||||||
{{ item?.result?.media?.length }}
|
{{ item?.result?.media?.length }}
|
||||||
@@ -74,17 +152,37 @@
|
|||||||
<template v-slot:expanded-row="{ columns, item }">
|
<template v-slot:expanded-row="{ columns, item }">
|
||||||
<tr>
|
<tr>
|
||||||
<td :colspan="columns.length" class="pa-0">
|
<td :colspan="columns.length" class="pa-0">
|
||||||
<v-data-table density="compact" class="sub-table elevation-0 bg-blue-grey-lighten-5"
|
<v-data-table
|
||||||
:items="item?.result?.media" item-key="key" hide-default-footer :headers="fileHeaders">
|
density="compact"
|
||||||
|
class="sub-table elevation-0 bg-blue-grey-lighten-5"
|
||||||
|
:items="item?.result?.media"
|
||||||
|
item-key="key"
|
||||||
|
hide-default-footer
|
||||||
|
:headers="fileHeaders"
|
||||||
|
>
|
||||||
<template v-slot:item.preview="{ item: media }">
|
<template v-slot:item.preview="{ item: media }">
|
||||||
<a :href="media.urls[0]" target="_blank">
|
<a :href="media.urls[0]" target="_blank">
|
||||||
<template v-if="media._mimetype?.startsWith('image/')">
|
<template
|
||||||
<v-img :src="media.urls[0]" max-width="150" max-height="250" class="mx-auto"></v-img>
|
v-if="media._mimetype?.startsWith('image/')"
|
||||||
|
>
|
||||||
|
<v-img
|
||||||
|
:src="media.urls[0]"
|
||||||
|
max-width="150"
|
||||||
|
max-height="250"
|
||||||
|
class="mx-auto"
|
||||||
|
></v-img>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="media._mimetype?.startsWith('video/')">
|
<template
|
||||||
<video :src="media.urls[0]" controls style="max-width: 150px; max-height: 200px;"
|
v-else-if="
|
||||||
class="mx-auto"></video>
|
media._mimetype?.startsWith('video/')
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<video
|
||||||
|
:src="media.urls[0]"
|
||||||
|
controls
|
||||||
|
style="max-width: 150px; max-height: 200px"
|
||||||
|
class="mx-auto"
|
||||||
|
></video>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<span>{{ media?.properties?.id }}</span>
|
<span>{{ media?.properties?.id }}</span>
|
||||||
@@ -92,7 +190,9 @@
|
|||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:item.hash="{ item: media }">
|
<template v-slot:item.hash="{ item: media }">
|
||||||
<span style="font-size: small;">{{ media?.properties?.hash }}</span>
|
<span style="font-size: small">{{
|
||||||
|
media?.properties?.hash
|
||||||
|
}}</span>
|
||||||
</template>
|
</template>
|
||||||
</v-data-table>
|
</v-data-table>
|
||||||
</td>
|
</td>
|
||||||
@@ -102,7 +202,6 @@
|
|||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
@@ -117,7 +216,8 @@ import WelcomeCard from "@/components/WelcomeCard.vue";
|
|||||||
export default {
|
export default {
|
||||||
name: "ArchiveSearchView",
|
name: "ArchiveSearchView",
|
||||||
components: {
|
components: {
|
||||||
PermissionNeeded, WelcomeCard
|
PermissionNeeded,
|
||||||
|
WelcomeCard,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -129,7 +229,12 @@ export default {
|
|||||||
loading: false,
|
loading: false,
|
||||||
itemsPerPage: 5,
|
itemsPerPage: 5,
|
||||||
totalItems: 0,
|
totalItems: 0,
|
||||||
pageOptions: [{ value: 5, title: '5' }, { value: 10, title: '10' }, { value: 25, title: '25' }, { value: 50, title: '50' }],
|
pageOptions: [
|
||||||
|
{ value: 5, title: "5" },
|
||||||
|
{ value: 10, title: "10" },
|
||||||
|
{ value: 25, title: "25" },
|
||||||
|
{ value: 50, title: "50" },
|
||||||
|
],
|
||||||
headers: [
|
headers: [
|
||||||
{ title: "URL", value: "url" },
|
{ title: "URL", value: "url" },
|
||||||
{ title: "Result", value: "result" },
|
{ title: "Result", value: "result" },
|
||||||
@@ -137,12 +242,17 @@ export default {
|
|||||||
{ title: "Deleted", value: "store_until", width: "150px" },
|
{ title: "Deleted", value: "store_until", width: "150px" },
|
||||||
{ title: "Size (MB)", value: "size" },
|
{ title: "Size (MB)", value: "size" },
|
||||||
{ title: "Files", value: "files" },
|
{ title: "Files", value: "files" },
|
||||||
{ title: '', key: 'data-table-expand' },
|
{ title: "", key: "data-table-expand" },
|
||||||
],
|
],
|
||||||
fileHeaders: [
|
fileHeaders: [
|
||||||
{ title: 'Preview', value: 'preview', align: 'center' },
|
{ title: "Preview", value: "preview", align: "center" },
|
||||||
{ title: 'Hash', value: 'hash', align: 'end', width: '150px' },
|
{ title: "Hash", value: "hash", align: "end", width: "150px" },
|
||||||
{ title: 'Size', value: 'properties.size', align: 'end', width: '150px' }
|
{
|
||||||
|
title: "Size",
|
||||||
|
value: "properties.size",
|
||||||
|
align: "end",
|
||||||
|
width: "150px",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
serverItems: [],
|
serverItems: [],
|
||||||
snackbar: false,
|
snackbar: false,
|
||||||
@@ -154,14 +264,14 @@ export default {
|
|||||||
return this.$store.state.user;
|
return this.$store.state.user;
|
||||||
},
|
},
|
||||||
featureEnabled() {
|
featureEnabled() {
|
||||||
const read = this.user?.permissions?.['all']?.read
|
const read = this.user?.permissions?.["all"]?.read;
|
||||||
if (read === true) {
|
if (read === true) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (Array.isArray(read) && read.length > 0) {
|
if (Array.isArray(read) && read.length > 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return this.user?.permissions?.['all']?.read_public
|
return this.user?.permissions?.["all"]?.read_public;
|
||||||
},
|
},
|
||||||
validUrl() {
|
validUrl() {
|
||||||
return this.queryUrl && this.urlValidator(this.queryUrl) === true;
|
return this.queryUrl && this.urlValidator(this.queryUrl) === true;
|
||||||
@@ -178,7 +288,7 @@ export default {
|
|||||||
if (!this.validUrl) return;
|
if (!this.validUrl) return;
|
||||||
this.tableSearch = `${this.queryUrl}${this.queryAfter}${this.queryBefore}`;
|
this.tableSearch = `${this.queryUrl}${this.queryAfter}${this.queryBefore}`;
|
||||||
},
|
},
|
||||||
loadItems({ page, itemsPerPage, sortBy }) {
|
loadItems({ page, itemsPerPage, _sortBy }) {
|
||||||
if (!this.validUrl || this.loading === true) return;
|
if (!this.validUrl || this.loading === true) return;
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
||||||
@@ -194,17 +304,17 @@ export default {
|
|||||||
params.append("archived_before", this.queryBefore.toISOString());
|
params.append("archived_before", this.queryBefore.toISOString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fetch(`${this.$store.state.API_ENDPOINT}/url/search?${params}`, {
|
fetch(`${this.$store.state.API_ENDPOINT}/url/search?${params}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: `Bearer ${this.$store.state.access_token}`,
|
Authorization: `Bearer ${this.$store.state.access_token}`,
|
||||||
}
|
},
|
||||||
}).then(response => response.json())
|
})
|
||||||
.then(items => {
|
.then((response) => response.json())
|
||||||
|
.then((items) => {
|
||||||
if (!Array.isArray(items)) {
|
if (!Array.isArray(items)) {
|
||||||
throw (`Unexpected response format from API`);
|
throw `Unexpected response format from API`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Estimate totalItems if not provided by the API
|
// Estimate totalItems if not provided by the API
|
||||||
@@ -215,7 +325,7 @@ export default {
|
|||||||
this.totalItems = (page + 1) * itemsPerPage; // Assume there are more items
|
this.totalItems = (page + 1) * itemsPerPage; // Assume there are more items
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
console.error("/url/search", error);
|
console.error("/url/search", error);
|
||||||
this.snackbarMessage = `Error searching for archives: ${error}`;
|
this.snackbarMessage = `Error searching for archives: ${error}`;
|
||||||
this.snackbar = true;
|
this.snackbar = true;
|
||||||
@@ -223,7 +333,7 @@ export default {
|
|||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -3,13 +3,16 @@
|
|||||||
<WelcomeCard />
|
<WelcomeCard />
|
||||||
<v-container class="pane" v-if="user?.active && featureEnabled">
|
<v-container class="pane" v-if="user?.active && featureEnabled">
|
||||||
<v-card>
|
<v-card>
|
||||||
<v-card-title class="text-center">
|
<v-card-title class="text-center"> Archive a single URL </v-card-title>
|
||||||
Archive a single URL
|
|
||||||
</v-card-title>
|
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col cols="12">
|
<v-col cols="12">
|
||||||
<v-text-field v-model="url" label="URL" required :rules="[urlValidator]"></v-text-field>
|
<v-text-field
|
||||||
|
v-model="url"
|
||||||
|
label="URL"
|
||||||
|
required
|
||||||
|
:rules="[urlValidator]"
|
||||||
|
></v-text-field>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="4">
|
<v-col cols="12" md="4">
|
||||||
<v-radio-group v-model="public" inline>
|
<v-radio-group v-model="public" inline>
|
||||||
@@ -18,11 +21,25 @@
|
|||||||
</v-radio-group>
|
</v-radio-group>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="4">
|
<v-col cols="12" md="4">
|
||||||
<v-select v-model="group" label="Group" :items="availableGroups" density="compact"></v-select>
|
<v-select
|
||||||
|
v-model="group"
|
||||||
|
label="Group"
|
||||||
|
:items="availableGroups"
|
||||||
|
density="compact"
|
||||||
|
></v-select>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="4" class="text-right">
|
<v-col cols="12" md="4" class="text-right">
|
||||||
<v-btn @click="archiveUrl" color="teal"
|
<v-btn
|
||||||
:disabled="!validUrl || loadingArchive || (group == 'please select') || maxedOutMBs || maxedOutURLs">
|
@click="archiveUrl"
|
||||||
|
color="teal"
|
||||||
|
:disabled="
|
||||||
|
!validUrl ||
|
||||||
|
loadingArchive ||
|
||||||
|
group == 'please select' ||
|
||||||
|
maxedOutMBs ||
|
||||||
|
maxedOutURLs
|
||||||
|
"
|
||||||
|
>
|
||||||
Archive
|
Archive
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-col>
|
</v-col>
|
||||||
@@ -30,49 +47,97 @@
|
|||||||
<v-row>
|
<v-row>
|
||||||
<v-col cols="12">
|
<v-col cols="12">
|
||||||
<p v-if="loadingArchive">
|
<p v-if="loadingArchive">
|
||||||
<v-progress-circular color="teal" indeterminate></v-progress-circular>
|
<v-progress-circular
|
||||||
Archive in progress <span v-if="taskId">task id = <code>{{ taskId }}</code></span>
|
color="teal"
|
||||||
|
indeterminate
|
||||||
|
></v-progress-circular>
|
||||||
|
Archive in progress
|
||||||
|
<span v-if="taskId"
|
||||||
|
>task id = <code>{{ taskId }}</code></span
|
||||||
|
>
|
||||||
</p>
|
</p>
|
||||||
<v-alert color="success" icon="mdi-information" v-if="archiveResult">
|
<v-alert
|
||||||
|
color="success"
|
||||||
|
icon="mdi-information"
|
||||||
|
v-if="archiveResult"
|
||||||
|
>
|
||||||
Archived successfully with id {{ archiveResult.id }}
|
Archived successfully with id {{ archiveResult.id }}
|
||||||
<span v-if="urlFromResult"> available <a :href="urlFromResult" target="_blank">here</a>.</span>
|
<span v-if="urlFromResult">
|
||||||
|
available
|
||||||
|
<a :href="urlFromResult" target="_blank">here</a>.</span
|
||||||
|
>
|
||||||
<span v-if="!urlFromResult">no archived content to show.</span>
|
<span v-if="!urlFromResult">no archived content to show.</span>
|
||||||
</v-alert>
|
</v-alert>
|
||||||
<v-alert color="warning" icon="mdi-alert" v-if="archiveFailure">
|
<v-alert color="warning" icon="mdi-alert" v-if="archiveFailure">
|
||||||
Failure: {{ archiveFailure }}
|
Failure: {{ archiveFailure }}
|
||||||
</v-alert>
|
</v-alert>
|
||||||
<p v-if="validUrl">
|
<p v-if="validUrl">
|
||||||
You can <strong v-if="archiveFailure">still</strong> <router-link
|
You can <strong v-if="archiveFailure">still</strong>
|
||||||
:to="`/archives?url=${encodeURIComponent(url)}`" target="_blank"><v-icon>mdi-open-in-new</v-icon> search
|
<router-link
|
||||||
for
|
:to="`/archives?url=${encodeURIComponent(url)}`"
|
||||||
archives</router-link> of
|
target="_blank"
|
||||||
this URL.
|
><v-icon>mdi-open-in-new</v-icon> search for
|
||||||
|
archives</router-link
|
||||||
|
>
|
||||||
|
of this URL.
|
||||||
</p>
|
</p>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" sm="12" class="pt-0" v-if="group != 'please select'">
|
<v-col cols="12" sm="12" class="pt-0" v-if="group != 'please select'">
|
||||||
<span>
|
<span>
|
||||||
<code>{{ group }}</code><br />
|
<code>{{ group }}</code
|
||||||
|
><br />
|
||||||
<span class="text-medium-emphasis mb-1">
|
<span class="text-medium-emphasis mb-1">
|
||||||
{{ groupPermissions.description }}
|
{{ groupPermissions.description }}
|
||||||
</span>
|
</span>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
Monthly URLs: <strong>{{ groupUsage.monthly_urls || 0 }}</strong>
|
Monthly URLs:
|
||||||
|
<strong>{{ groupUsage.monthly_urls || 0 }}</strong>
|
||||||
out of
|
out of
|
||||||
<strong>{{ displayPermissionValue(groupPermissions?.max_monthly_urls, " URLs") }}</strong>
|
<strong>{{
|
||||||
<v-chip v-if="maxedOutURLs" label class="ml-2" color="red" density="comfortable" size="small">maxed
|
displayPermissionValue(
|
||||||
out</v-chip>
|
groupPermissions?.max_monthly_urls,
|
||||||
|
" URLs"
|
||||||
|
)
|
||||||
|
}}</strong>
|
||||||
|
<v-chip
|
||||||
|
v-if="maxedOutURLs"
|
||||||
|
label
|
||||||
|
class="ml-2"
|
||||||
|
color="red"
|
||||||
|
density="comfortable"
|
||||||
|
size="small"
|
||||||
|
>maxed out</v-chip
|
||||||
|
>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
Monthly MBs:
|
Monthly MBs:
|
||||||
<strong>{{ groupUsage.monthly_mbs || 0 }}</strong>
|
<strong>{{ groupUsage.monthly_mbs || 0 }}</strong>
|
||||||
out of
|
out of
|
||||||
<strong>{{ displayPermissionValue(groupPermissions?.max_monthly_mbs, " MBs") }}</strong>
|
<strong>{{
|
||||||
<v-chip v-if="maxedOutMBs" label class="ml-2" color="red" density="comfortable" size="small">maxed
|
displayPermissionValue(
|
||||||
out</v-chip>
|
groupPermissions?.max_monthly_mbs,
|
||||||
|
" MBs"
|
||||||
|
)
|
||||||
|
}}</strong>
|
||||||
|
<v-chip
|
||||||
|
v-if="maxedOutMBs"
|
||||||
|
label
|
||||||
|
class="ml-2"
|
||||||
|
color="red"
|
||||||
|
density="comfortable"
|
||||||
|
size="small"
|
||||||
|
>maxed out</v-chip
|
||||||
|
>
|
||||||
</li>
|
</li>
|
||||||
<li>We will store archives for: <strong>{{
|
<li>
|
||||||
displayPermissionValue(groupPermissions?.max_archive_lifespan_months, " months") }}</strong>
|
We will store archives for:
|
||||||
|
<strong>{{
|
||||||
|
displayPermissionValue(
|
||||||
|
groupPermissions?.max_archive_lifespan_months,
|
||||||
|
" months"
|
||||||
|
)
|
||||||
|
}}</strong>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</span>
|
</span>
|
||||||
@@ -80,7 +145,12 @@
|
|||||||
</v-row>
|
</v-row>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
<SnackBar :message="snackbarMessage" :show="snackbar" :color="snackbarColor" @update:show="snackbar = $event" />
|
<SnackBar
|
||||||
|
:message="snackbarMessage"
|
||||||
|
:show="snackbar"
|
||||||
|
:color="snackbarColor"
|
||||||
|
@update:show="snackbar = $event"
|
||||||
|
/>
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -93,11 +163,13 @@ import WelcomeCard from "@/components/WelcomeCard.vue";
|
|||||||
export default {
|
export default {
|
||||||
name: "ArchiveUrlView",
|
name: "ArchiveUrlView",
|
||||||
components: {
|
components: {
|
||||||
SnackBar, PermissionNeeded, WelcomeCard
|
SnackBar,
|
||||||
|
PermissionNeeded,
|
||||||
|
WelcomeCard,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
url: "",
|
url: this.$route.query.url || "",
|
||||||
public: false,
|
public: false,
|
||||||
group: "please select",
|
group: "please select",
|
||||||
loadingArchive: false,
|
loadingArchive: false,
|
||||||
@@ -131,8 +203,8 @@ export default {
|
|||||||
availableGroups() {
|
availableGroups() {
|
||||||
const permissions = this.$store.state.user?.permissions || {};
|
const permissions = this.$store.state.user?.permissions || {};
|
||||||
return Object.keys(permissions)
|
return Object.keys(permissions)
|
||||||
.filter(group => group !== "all" && permissions[group].archive_url)
|
.filter((group) => group !== "all" && permissions[group].archive_url)
|
||||||
.map(g => ({ title: g, value: g }));
|
.map((g) => ({ title: g, value: g }));
|
||||||
},
|
},
|
||||||
globalUsage() {
|
globalUsage() {
|
||||||
return this.$store.state.user?.usage || {};
|
return this.$store.state.user?.usage || {};
|
||||||
@@ -151,21 +223,28 @@ export default {
|
|||||||
},
|
},
|
||||||
maxedOutMBs() {
|
maxedOutMBs() {
|
||||||
if (this.groupPermissions.max_monthly_mbs === -1) return false;
|
if (this.groupPermissions.max_monthly_mbs === -1) return false;
|
||||||
return this.groupUsage.monthly_mbs >= this.groupPermissions.max_monthly_mbs;
|
return (
|
||||||
|
this.groupUsage.monthly_mbs >= this.groupPermissions.max_monthly_mbs
|
||||||
|
);
|
||||||
},
|
},
|
||||||
maxedOutURLs() {
|
maxedOutURLs() {
|
||||||
if (this.groupPermissions.max_monthly_urls === -1) return false;
|
if (this.groupPermissions.max_monthly_urls === -1) return false;
|
||||||
return this.groupUsage.monthly_urls >= this.groupPermissions.max_monthly_urls;
|
return (
|
||||||
|
this.groupUsage.monthly_urls >= this.groupPermissions.max_monthly_urls
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
url(val) {
|
url(_val) {
|
||||||
this.archiveResult = null;
|
this.archiveResult = null;
|
||||||
this.archiveFailure = null;
|
this.archiveFailure = null;
|
||||||
this.taskId = null;
|
this.taskId = null;
|
||||||
if (this.loadingArchive) {
|
if (this.loadingArchive) {
|
||||||
this.loadingArchive = false;
|
this.loadingArchive = false;
|
||||||
this.showSnackbar("Your previous archive will run in the background.", "yellow");
|
this.showSnackbar(
|
||||||
|
"Your previous archive will run in the background.",
|
||||||
|
"yellow"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -192,20 +271,23 @@ export default {
|
|||||||
group_id: this.group,
|
group_id: this.group,
|
||||||
public: this.public,
|
public: this.public,
|
||||||
tags: [],
|
tags: [],
|
||||||
})
|
}),
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then((response) => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`API error: ${response.statusText}`);
|
throw new Error(`API error: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
return response.json();
|
return response.json();
|
||||||
})
|
})
|
||||||
.then(res => {
|
.then((res) => {
|
||||||
this.taskId = res.id;
|
this.taskId = res.id;
|
||||||
this.showSnackbar(`Your URL is being archived with id ${this.taskId}!`, "green");
|
this.showSnackbar(
|
||||||
|
`Your URL is being archived with id ${this.taskId}!`,
|
||||||
|
"green"
|
||||||
|
);
|
||||||
this.pollForArchiveResults();
|
this.pollForArchiveResults();
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
console.error("/archive ", error);
|
console.error("/archive ", error);
|
||||||
this.showSnackbar(`Unable to archive URL: ${error.message}`);
|
this.showSnackbar(`Unable to archive URL: ${error.message}`);
|
||||||
this.loadingArchive = false;
|
this.loadingArchive = false;
|
||||||
@@ -222,12 +304,15 @@ export default {
|
|||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: `Bearer ${this.$store.state.access_token}`,
|
Authorization: `Bearer ${this.$store.state.access_token}`,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then((response) => response.json())
|
||||||
.then(task => {
|
.then((task) => {
|
||||||
if (task.status === "SUCCESS") {
|
if (task.status === "SUCCESS") {
|
||||||
this.showSnackbar(`URL archived successfully with id ${task.id}!`, "green");
|
this.showSnackbar(
|
||||||
|
`URL archived successfully with id ${task.id}!`,
|
||||||
|
"green"
|
||||||
|
);
|
||||||
this.loadingArchive = false;
|
this.loadingArchive = false;
|
||||||
this.archiveResult = task;
|
this.archiveResult = task;
|
||||||
this.taskId = task.id;
|
this.taskId = task.id;
|
||||||
@@ -240,18 +325,22 @@ export default {
|
|||||||
setTimeout(poll, 5000); // Poll every 5 seconds
|
setTimeout(poll, 5000); // Poll every 5 seconds
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
console.error("/task ", error);
|
console.error("/task ", error);
|
||||||
this.showSnackbar(`Error checking archive status: ${error.message}`);
|
this.showSnackbar(
|
||||||
|
`Error checking archive status: ${error.message}`
|
||||||
|
);
|
||||||
this.loadingArchive = false;
|
this.loadingArchive = false;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
poll();
|
poll();
|
||||||
},
|
},
|
||||||
displayPermissionValue(value, extraWord) {
|
displayPermissionValue(value, extraWord) {
|
||||||
if (value === undefined) { return "not set"; }
|
if (value === undefined) {
|
||||||
|
return "not set";
|
||||||
|
}
|
||||||
return value == -1 ? "no limit" : value + extraWord;
|
return value == -1 ? "no limit" : value + extraWord;
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<PermissionNeeded v-if="user && !featureEnabled" feature="Archive Spreadsheets" />
|
<PermissionNeeded
|
||||||
<WelcomeCard/>
|
v-if="user && !featureEnabled"
|
||||||
|
feature="Archive Spreadsheets"
|
||||||
|
/>
|
||||||
|
<WelcomeCard />
|
||||||
<ArchiveSheet v-if="user?.active && featureEnabled" />
|
<ArchiveSheet v-if="user?.active && featureEnabled" />
|
||||||
<ManageSheets v-if="user?.active && featureEnabled" />
|
<ManageSheets v-if="user?.active && featureEnabled" />
|
||||||
</template>
|
</template>
|
||||||
@@ -14,7 +17,10 @@ import WelcomeCard from "@/components/WelcomeCard.vue";
|
|||||||
export default {
|
export default {
|
||||||
name: "HomeView",
|
name: "HomeView",
|
||||||
components: {
|
components: {
|
||||||
ArchiveSheet, ManageSheets, PermissionNeeded, WelcomeCard
|
ArchiveSheet,
|
||||||
|
ManageSheets,
|
||||||
|
PermissionNeeded,
|
||||||
|
WelcomeCard,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
user() {
|
user() {
|
||||||
@@ -22,7 +28,7 @@ export default {
|
|||||||
},
|
},
|
||||||
featureEnabled() {
|
featureEnabled() {
|
||||||
return this.user?.permissions?.["all"]?.archive_sheet;
|
return this.user?.permissions?.["all"]?.archive_sheet;
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user