create/add sheet logic

This commit is contained in:
msramalho
2024-11-13 12:33:37 +01:00
parent cec783c17b
commit 0b271e44ec
5 changed files with 488 additions and 151 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "firebase-archiver-2",
"version": "0.1.0",
"version": "0.2.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve --port 8081 --skip-plugins @vue/cli-plugin-eslint",

162
src/components/AddSheet.vue Normal file
View File

@@ -0,0 +1,162 @@
<template>
<v-row class="my-2">
<v-col cols="12" sm="12" class="ma-0 pb-0">
<v-text-field label="Google Sheets document name" v-model="sheetName" required
density="comfortable"></v-text-field>
</v-col>
<v-col v-if="!actionIsCreate" cols="12" sm="12" class="ma-0 py-0">
<v-text-field label="Existing Google Sheet URL/ID" v-model="sheetUrlId" required density="comfortable">
</v-text-field>
</v-col>
<v-col cols="6" sm="6" class="ma-0 py-0">
<v-select v-model="group" label="Group" :items="availableGroups" required density="comfortable"></v-select>
</v-col>
<v-col cols="6" sm="6" class="ma-0 py-0">
<v-select v-model="frequency" label="Archive frequency" :items="availableFrequencies" required
density="comfortable"></v-select>
</v-col>
<v-col cols="12" sm="12" class="text-right pt-0">
<small v-if="spreadsheetId">Detected Spreadsheet id: <code>{{ spreadsheetId }}</code></small>
</v-col>
<v-col cols="12" sm="12" class="text-right pt-0">
<v-progress-circular color="green" indeterminate class="mx-6" v-if="loading"></v-progress-circular>
<v-btn v-if="newSheetId" :href="`https://docs.google.com/spreadsheets/d/${newSheetId}`"
append-icon="mdi-open-in-new" :title="newSheetId" target="_blank" color="success" class="mx-2"
size="large">
open sheet
</v-btn>
<v-btn v-if="actionIsCreate" color="primary" size="large" :disabled="!requiredData"
@click="createSheet">Create</v-btn>
<v-btn v-if="!actionIsCreate" color="primary" size="large" :disabled="!requiredDataExisting"
@click="addExistingSheet">Add Existing Sheet</v-btn>
</v-col>
</v-row>
<SnackBar :message="snackbarMessage" :show="snackbar" :color="snackbarColor" @update:show="snackbar = $event" />
</template>
<script>
import SnackBar from "@/components/SnackBar.vue";
export default {
name: "AddSheet",
components: {
SnackBar,
},
props: {
actionIsCreate: {
type: Boolean,
required: true,
default: true,
}
},
data() {
return {
snackbar: false,
snackbarMessage: "",
snackbarColor: "red",
loading: false,
tab: '',
items: ['Create new Archiver Sheet', 'Add existing Sheets'],
sheetName: ``.trim(),
sheetUrlId: ``,
group: "please select",
availableFrequencies: ["daily", "hourly"].map(f => ({ title: f, value: f })),
frequency: "daily",
newSheetId: "",
};
},
computed: {
user() {
return this.$store.state.user;
},
requiredData() {
return this.sheetName && this.availableGroups?.some(g => g.value === this.group) && this.availableFrequencies?.some(f => f.value === this.frequency);
},
requiredDataExisting() {
return this.sheetName && this.spreadsheetId && this.availableGroups?.some(g => g.value === this.group) && this.availableFrequencies?.some(f => f.value === this.frequency);
},
availableGroups() {
return (this.$store.state.user?.groups || []).map(g => ({ title: g, value: g }));
},
spreadsheetId() {
if (
this.sheetUrlId.startsWith("http") &&
this.sheetUrlId.split("/").length >= 6
) {
return this.sheetUrlId.split("/")[5];
}
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.newSheetId = "";
this.$store.dispatch("add", this.sheetName).then((res) => {
console.log(res);
if (!res.success) throw new Error(res.result);
this.newSheetId = res.result;
this.addSheetToAPI(this.newSheetId);
}).catch((error) => {
console.error(error);
this.showSnackbar(`Unable to create sheet: ${error.message}`);
this.loading = false;
});
},
addExistingSheet() {
if (!this.requiredDataExisting) return;
if (this.loading) return;
this.loading = true;
this.addSheetToAPI(this.spreadsheetId);
},
addSheetToAPI(sheetId) {
fetch(`${this.$store.state.API_ENDPOINT}/sheet/create`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.$store.state.access_token}`,
},
body: JSON.stringify({
id: sheetId,
name: this.sheetName,
group_id: this.group,
frequency: this.frequency,
})
}).then(async response => {
const j = await response.json();
if (response.status === 201) {
this.showSnackbar(`Sheet created successfully!`, "green");
// this.$store.dispatch("refreshDocs"); //TODO: implement this
} else {
throw new Error(JSON.stringify(j));
}
}).catch(error => {
console.error("/sheet/create ", error);
this.showSnackbar(`Unable to save sheet to DB: ${error.message}`);
}).finally(() => {
this.loading = false;
this.sheetName = "";
this.sheetUrlId = "";
this.group = "please select";
});
}
},
};
</script>

View File

@@ -0,0 +1,161 @@
<template>
<v-container class="pane">
<v-card class="pa-3">
<v-card-title class="text-center">
Archive Google Spreadsheets
</v-card-title>
<v-tabs v-model="tab" bg-color="green-lighten-4" grow class="elevation-1 rounded">
<v-tab v-for="item in items" :key="item" :text="item" :value="item"></v-tab>
</v-tabs>
<v-tabs-window v-model="tab" class="elevation-1 rounded">
<v-tabs-window-item :value="items[0]">
<v-card-text>
<AddSheet :actionIsCreate="true" />
<v-expansion-panels elevation="0">
<v-expansion-panel>
<v-expansion-panel-title>Instructions</v-expansion-panel-title>
<v-expansion-panel-text>
<ol>
<li>Choose a sheet name;</li>
<li>Choose a group: this will impact where/how to archive;</li>
<li>Choose a frequency: how often to archive;</li>
<li>Press "create" and wait;</li>
<li>Sheet will appear in "Your Sheets" below.</li>
</ol>
<small>
<b>NB:</b> This new sheet will be shared with the
service account necessary for Bellingcat's archiving
server.
</small>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</v-card-text>
</v-tabs-window-item>
<v-tabs-window-item :value="items[1]">
<v-card-text>
<AddSheet :actionIsCreate="false" />
<v-expansion-panels elevation="0">
<v-expansion-panel>
<v-expansion-panel-title>Instructions</v-expansion-panel-title>
<v-expansion-panel-text>
<ol style="margin-bottom: 1em">
<li>
Invite
<code>bellingcat-auto-archiver-api@bellingcat-auto-archiver-b85db.iam.gserviceaccount.com</code>
into your spreadsheet
</li>
<li>
Make sure you have the following <b>mandatory</b> column names:
<ul>
<li><code>Link</code> where you will put the URLs.</li>
<li>
<code>Archive Status</code> to monitor progress and success
of archiver
</li>
<li>
<code>Archive location</code> where the link to the archived
content is added
</li>
</ul>
</li>
<li>
Add any of the following <b>optional</b> column names:
<ul>
<li>
<code>Archive date</code> info on when archiving occurred
</li>
<li>
<code>Thumbnail</code> an image preview from archived media
</li>
<li>
<code>Upload timestamp</code> online content creation date
</li>
<li><code>Upload title</code> title</li>
<li><code>Textual content</code> text content</li>
<li><code>Screenshot</code> link to page screenshot</li>
<li>
<code>Hash</code> content hash (for integrity purposes)
</li>
</ul>
</li>
<li>Choose a name to associate with this Sheet</li>
<li>Paste the Google Sheet URL</li>
<li>Press "enable" to add the Google Sheet to your list</li>
<li>
Manually check archiving is working and re-check the steps above
if it is not
</li>
</ol>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</v-card-text>
</v-tabs-window-item>
</v-tabs-window>
</v-card>
</v-container>
<v-container class="pane-l">
<v-card>
<v-card-title class="text-center">
<h4>Your Sheets</h4>
</v-card-title>
<v-table>
<thead>
<tr>
<th class="text-left">Sheet Name</th>
<th class="text-left">Action</th>
</tr>
</thead>
<tbody>
<tr>
<td>Sheet 1</td>
<td>
<v-btn color="primary" @click="archiveSheet('Sheet 1')">Archive</v-btn>
</td>
</tr>
<tr>
<td>Sheet 2</td>
<td>
<v-btn color="primary" @click="archiveSheet('Sheet 2')">Archive</v-btn>
</td>
</tr>
<tr>
<td>Sheet 3</td>
<td>
<v-btn color="primary" @click="archiveSheet('Sheet 3')">Archive</v-btn>
</td>
</tr>
</tbody>
</v-table>
</v-card>
</v-container>
</template>
<script>
import AddSheet from "@/components/AddSheet.vue";
export default {
name: "ArchiveSheet",
components: {
AddSheet
},
data() {
return {
tab: '',
items: ['Create new Archiver Sheet', 'Add existing Sheets'],
};
},
computed: {
user() {
return this.$store.state.user;
},
},
};
</script>

View File

@@ -54,7 +54,8 @@ export default createStore({
docs: [],
loading: false,
errorMessage: "",
API_ENDPOINT: "https://auto-archiver-api.bellingcat.com"
// API_ENDPOINT: "https://auto-archiver-api.bellingcat.com"
API_ENDPOINT: "http://localhost:8004"
},
mutations: {
setUser(state, user) {
@@ -64,6 +65,10 @@ export default createStore({
setUserActiveState(state, active) {
state.user.active = active;
},
setUserGroups(state, groups) {
state.user.groups = groups;
saveToLocalStorage(state);
},
setDocs(state, docs) {
state.docs = docs;
},
@@ -89,6 +94,7 @@ export default createStore({
commit("setUser", response.user);
dispatch("checkActiveUser");
dispatch("checkUserGroups");
dispatch("getDocs");
}
@@ -148,6 +154,27 @@ export default createStore({
}
},
async checkUserGroups({ state, commit }) {
try {
commit("setErrorMessage", "");
const r = await fetch(
`${state.API_ENDPOINT}/groups`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${state.access_token}`,
},
}
);
const response = await r.json();
commit("setUserGroups", response);
} catch (error) {
console.error("checkUserGroups (firebase.js): ", error);
commit("setErrorMessage", "Unable to fetch user groups from the API");
}
},
async getDocs({ state, commit }) {
if (!state.user || !state.user.active) {
return;
@@ -212,174 +239,157 @@ export default createStore({
}
},
async add({ state, dispatch, commit }, { name }) {
async add({ state, dispatch, commit }, name) {
commit("setLoading", true);
try {
// create new sheet
const newSheet = await gapi.client.sheets.spreadsheets.create({
properties: {
title: name,
},
});
return new Promise(async (resolve, reject) => {
try {
// create new sheet
const newSheet = await gapi.client.sheets.spreadsheets.create({
properties: {
title: name,
},
});
const spreadsheetId = newSheet.result.spreadsheetId;
const spreadsheetId = newSheet.result.spreadsheetId;
const userEnteredFormat = {
textFormat: {
bold: true,
},
};
const userEnteredFormat = {
textFormat: {
bold: true,
},
};
// add header row
await gapi.client.sheets.spreadsheets.batchUpdate(
{
spreadsheetId: spreadsheetId,
},
{
requests: [
{
updateCells: {
rows: [
{
values: [
{
userEnteredValue: {
stringValue: "Link",
// add header row
await gapi.client.sheets.spreadsheets.batchUpdate(
{
spreadsheetId: spreadsheetId,
},
{
requests: [
{
updateCells: {
rows: [
{
values: [
{
userEnteredValue: {
stringValue: "Link",
},
userEnteredFormat,
},
userEnteredFormat,
},
{
userEnteredValue: {
stringValue: "Archive status",
{
userEnteredValue: {
stringValue: "Archive status",
},
userEnteredFormat,
},
userEnteredFormat,
},
{
userEnteredValue: {
stringValue: "Destination folder",
{
userEnteredValue: {
stringValue: "Destination folder",
},
userEnteredFormat,
},
userEnteredFormat,
},
{
userEnteredValue: {
stringValue: "Archive location",
{
userEnteredValue: {
stringValue: "Archive location",
},
userEnteredFormat,
},
userEnteredFormat,
},
{
userEnteredValue: {
stringValue: "Archive date",
{
userEnteredValue: {
stringValue: "Archive date",
},
userEnteredFormat,
},
userEnteredFormat,
},
{
userEnteredValue: {
stringValue: "Thumbnail",
{
userEnteredValue: {
stringValue: "Thumbnail",
},
userEnteredFormat,
},
userEnteredFormat,
},
{
userEnteredValue: {
stringValue: "Upload timestamp",
{
userEnteredValue: {
stringValue: "Upload timestamp",
},
userEnteredFormat,
},
userEnteredFormat,
},
{
userEnteredValue: {
stringValue: "Upload title",
{
userEnteredValue: {
stringValue: "Upload title",
},
userEnteredFormat,
},
userEnteredFormat,
},
{
userEnteredValue: {
stringValue: "Textual content",
{
userEnteredValue: {
stringValue: "Textual content",
},
userEnteredFormat,
},
userEnteredFormat,
},
{
userEnteredValue: {
stringValue: "Screenshot",
{
userEnteredValue: {
stringValue: "Screenshot",
},
userEnteredFormat,
},
userEnteredFormat,
},
{
userEnteredValue: {
stringValue: "Hash",
{
userEnteredValue: {
stringValue: "Hash",
},
userEnteredFormat,
},
userEnteredFormat,
},
// {
// userEnteredValue: {
// stringValue: "WACZ",
// },
// userEnteredFormat,
// },
// {
// userEnteredValue: {
// stringValue: "Replaywebpage",
// },
// userEnteredFormat,
// },
],
},
],
fields:
"userEnteredValue.stringValue,userEnteredFormat.textFormat.bold",
start: {
sheetId: 0,
rowIndex: 0,
columnIndex: 0,
},
},
},
{
addProtectedRange: {
protectedRange: {
range: {
],
},
],
fields:
"userEnteredValue.stringValue,userEnteredFormat.textFormat.bold",
start: {
sheetId: 0,
startRowIndex: 0,
endRowIndex: 1,
startColumnIndex: 0,
endColumnIndex: 11,
rowIndex: 0,
columnIndex: 0,
},
description:
"Protecting header row (needed for auto-archiver), do not modify archiving column names, you can add and move columns around when no 'Archive in Progress' is present in the 'Archive status' column.",
warningOnly: true,
},
},
},
],
{
addProtectedRange: {
protectedRange: {
range: {
sheetId: 0,
startRowIndex: 0,
endRowIndex: 1,
startColumnIndex: 0,
endColumnIndex: 11,
},
description:
"Protecting header row (needed for auto-archiver), do not modify archiving column names, you can add and move columns around when no 'Archive in Progress' is present in the 'Archive status' column.",
warningOnly: true,
},
},
},
],
}
);
// add permissions
await gapi.client.drive.permissions.create({
fileId: spreadsheetId,
resource: {
role: "writer",
type: "user",
emailAddress:
"bellingcat-auto-archiver-api@bellingcat-auto-archiver-b85db.iam.gserviceaccount.com",
},
});
resolve({ success: true, result: spreadsheetId });
} catch (error) {
console.error("add (firebase.js): ", error);
if (error.status === 401) {
await dispatch("signout");
}
);
// add permissions
await gapi.client.drive.permissions.create({
fileId: spreadsheetId,
resource: {
role: "writer",
type: "user",
emailAddress:
"bellingcat-auto-archiver-api@bellingcat-auto-archiver-b85db.iam.gserviceaccount.com",
},
});
const col = await collection(firebaseFirestore, "sheets");
await addDoc(col, {
sheetId: spreadsheetId,
url: newSheet.result.spreadsheetUrl,
timestamp: Date.now(),
uid: state.user.uid,
email: state.user.email,
lastArchived: null,
name: name,
});
dispatch("getDocs");
} catch (error) {
console.error("add (firebase.js): ", error);
}
reject({ success: false, result: error });
}
commit("setLoading", false);
});
},
async enable({ state, dispatch, commit }, { spreadsheetId }) {
@@ -456,7 +466,9 @@ export default createStore({
if (expired) {
store.dispatch("signout");
} else {
//TODO: merge these into single endpoint in the future
store.dispatch("checkActiveUser");
store.dispatch("checkUserGroups");
store.dispatch("getDocs");
}
}).catch((error) => {

View File

@@ -1,11 +1,12 @@
<template>
<ArchiveSheet v-if="user?.active" />
<v-container class="pane">
<v-row>
<v-col>
<v-card style="margin-bottom: 1em">
<v-card-text>
<v-alert color="#f2d97c" icon="mdi-alert">
This is a pre-release prototype demo service provided on a
This is still a pre-release prototype demo service provided on a
best-effort basis. Do not use for mission critical or sensitive
data.
</v-alert>
@@ -161,11 +162,12 @@
<script>
import DocList from "@/components/DocList.vue";
import ArchiveSheet from "@/components/ArchiveSheet.vue";
export default {
name: "SheetView",
components: {
DocList,
DocList, ArchiveSheet
},
data() {
return {