diff --git a/other/train-to-cloud-city/README.md b/other/train-to-cloud-city/README.md index ca2e4550..0828b0e0 100644 --- a/other/train-to-cloud-city/README.md +++ b/other/train-to-cloud-city/README.md @@ -5,7 +5,7 @@ "Train to Cloud City" demo project is designed to physically represent mixing and matching [Google Cloud Platform](https://console.cloud.google.com/) resources that resolve common development use cases (patterns) in a kinesthetically whimsical fashion. -The challenge is putting together physical components (train cars/blocks) that match control statements (coinciding GCP resources), +The challenge is putting together physical components (train cars/blocks) that match control statements (coinciding GCP resources), which will validate if the pattern fits. For every validated component, the train will move forward to the next stop till it makes the entire way around the circle track. ## Getting Started @@ -17,10 +17,10 @@ which will validate if the pattern fits. For every validated component, the trai ## Technologies & hardware -* [Google Cloud Platform](https://console.cloud.google.com/) -* [Cloud Firestore](https://cloud.google.com/firestore/docs/) -* [Firebase](https://firebase.google.com/) -* [Terraform](https://cloud.google.com/docs/terraform) +- [Google Cloud Platform](https://console.cloud.google.com/) +- [Cloud Firestore](https://cloud.google.com/firestore/docs/) +- [Firebase](https://firebase.google.com/) +- [Terraform](https://cloud.google.com/docs/terraform) * [City Freight Lego Train](https://www.lego.com/en-us/product/freight-train-60336) * [Sparkfun RFID readers](https://www.sparkfun.com/products/9963) @@ -33,5 +33,5 @@ Check out our [CONTRIBUTOR](../../CONTRIBUTING.md) guide! ## Disclaimer -This is not an official Google project. "Train to Cloud City" demo project is +This is not an official Google project. "Train to Cloud City" demo project is designed particularly for in-person learning experiences at Google Cloud Next'24. diff --git a/other/train-to-cloud-city/app/client/Dockerfile b/other/train-to-cloud-city/app/client/Dockerfile new file mode 100644 index 00000000..92d36ee9 --- /dev/null +++ b/other/train-to-cloud-city/app/client/Dockerfile @@ -0,0 +1,29 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM node:18.16.0-alpine3.17 + +WORKDIR /app + +ENV PATH /app/node_modules/.bin:$PATH + +COPY package.json ./ + +COPY package-lock.json ./ + +RUN npm install --silent + +COPY . ./ + +CMD ["npm", "start"] diff --git a/other/train-to-cloud-city/app/client/README.md b/other/train-to-cloud-city/app/client/README.md index 0efc53e7..68985f74 100644 --- a/other/train-to-cloud-city/app/client/README.md +++ b/other/train-to-cloud-city/app/client/README.md @@ -24,8 +24,9 @@ REACT_APP_FIREBASE_MEASUREMENT_ID= 3. Install npm dependencies and start up the client app. ```bash -npm install +npm install npm run build npm start ``` + Your browser should open up to `localhost:3000`. diff --git a/other/train-to-cloud-city/app/client/public/index.html b/other/train-to-cloud-city/app/client/public/index.html index 13d699f7..864a12f9 100644 --- a/other/train-to-cloud-city/app/client/public/index.html +++ b/other/train-to-cloud-city/app/client/public/index.html @@ -19,12 +19,15 @@ - + - + [Next '24] Train to Cloud City diff --git a/other/train-to-cloud-city/app/client/src/App.css b/other/train-to-cloud-city/app/client/src/App.css index 34c94c5a..6696274e 100644 --- a/other/train-to-cloud-city/app/client/src/App.css +++ b/other/train-to-cloud-city/app/client/src/App.css @@ -15,17 +15,35 @@ */ :root { + --font: "Pixelify Sans"; --melanzane: #211e20; --smoky: #555568; --lemonGrass: #a0a08b; --lilyWhite: #e9efec; --darkOrange: #ff8100; + --cinnabar: #e94434; + --dodgerblue: #4286f5; + --selectiveyellow: #fbbb06; + --eucalyptus: #34a752; + --font: "Pixelify Sans", sans-serif; +} +button { + padding: 10px; + margin: 5px; + font-family: var(--font); + font-size: larger; + background: var(--selectiveyellow); + border-bottom-left-radius: 10px; + border-bottom-right-radius: 10px; + border-top-left-radius: 10px; + border-top-right-radius: 10px; } body { - background-color: var(--melanzane); - color: var(--lilyWhite); - font-family: monospace; + background-color: var(--lilyWhite); + color: var(--melanzane); + font-family: "Pixelify Sans", sans-serif; + font-optical-sizing: auto; margin: 0; } diff --git a/other/train-to-cloud-city/app/client/src/actions/coreActions.js b/other/train-to-cloud-city/app/client/src/actions/coreActions.js index 34db3694..0bb9816f 100644 --- a/other/train-to-cloud-city/app/client/src/actions/coreActions.js +++ b/other/train-to-cloud-city/app/client/src/actions/coreActions.js @@ -11,11 +11,30 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. + import { createAsyncThunk } from "@reduxjs/toolkit"; import firebaseInstance from "../Firebase"; /**** Getters ****/ +/** + * ----------------- + * getWorldState + * ----------------- + */ +export const getWorldState = createAsyncThunk( + "getWorldState", + async (collectionName = "global") => { + const ref = firebaseInstance.db.collection(collectionName); + const snapshot = await ref.get(); + let world = {}; + snapshot.docs.forEach((doc) => (world[doc.id] = doc.data())); + return world; + + return { state: world }; + }, +); + /** * ----------------- * getServices @@ -25,9 +44,7 @@ export const getServices = createAsyncThunk("getServices", async () => { const ref = firebaseInstance.db.collection("services"); const services = await ref.get().then((querySnapshot) => { let serviceList = []; - querySnapshot.docs.forEach((doc) => { - serviceList.push(`${doc.id}`); - }); + querySnapshot.docs.forEach((doc) => serviceList.push(`${doc.id}`)); return serviceList; }); @@ -50,121 +67,167 @@ export const getPatterns = createAsyncThunk("getPatterns", async () => { return { patterns }; }); +/**** Updates ****/ + /** * ----------------- - * getSinglePattern + * updateCargo * ----------------- + * Submit loaded cargo to firestore for validation */ -export const getSinglePattern = async(id) => { - const ref = firebaseInstance.db.collection("patterns").where("pattern_slug", "==", id); - const pattern = await ref.get(); - - let list = []; - pattern.docs.forEach((doc) => list.push(doc.data())); - - return list[0]; -}; +export const updateCargo = createAsyncThunk( + "updateCargo", + async (cargo = [], collectionName = "global") => { + const cargoRef = firebaseInstance.db + .collection(collectionName) + .doc("cargo"); + try { + await cargoRef.update({ actual_cargo: cargo }); + } catch (error) { + console.error(error); + } + }, +); /** * ----------------- - * getInitialWorldState + * stopMission * ----------------- + * Resets entire game and redirect user to homebase */ -export const getInitialWorldState = createAsyncThunk( - "getInitialWorldState", - async () => { - const ref = firebaseInstance.db.collection("global"); - - const state = await ref.get().then((querySnapshot) => { - let list = []; - querySnapshot.docs.forEach((doc) => list.push(doc.data())); - return list[0]; - }); - - return { state }; +export const stopMission = createAsyncThunk( + "stopMission", + async (collectionName = "global") => { + const cargoRef = firebaseInstance.db + .collection(collectionName) + .doc("cargo"); + const patternRef = firebaseInstance.db + .collection(collectionName) + .doc("proposal"); + + return Promise.all([ + cargoRef.update({ actual_cargo: [] }), + patternRef.update({ pattern_slug: "" }), + ]); }, ); +// ============== Listeners ============== // + /** * ----------------- - * getWorld + * worldStateUpdated * ----------------- */ -export const getWorld = async ({ dispatch, isSimulator, pattern }) => { - if(isSimulator) { - await worldSimulationStateUpdated(dispatch); - } else { - await worldStateUpdated(dispatch); - dispatch?.(updateSelectedPattern(pattern)); - } +export const worldStateUpdated = async ( + dispatch, + collectionName = "global", +) => { + const ref = firebaseInstance.db.collection(collectionName); + await ref.onSnapshot((snapshot) => { + snapshot.docChanges().forEach((change) => { + dispatch?.(getWorldState()); + }); + }); }; /** * ----------------- - * getWorldSimulation + * trainMailboxUpdated * ----------------- */ -export const getWorldSimulation = createAsyncThunk( - "getWorldSimulation", - async (changeType) => { - const ref = firebaseInstance.db.collection("global_simulation"); - const simulationState = await ref.get().then((querySnapshot) => { - let list = []; - querySnapshot.docs.forEach((doc) => list.push(doc.data())); - return list[0]; - }); - - return { simulationState: { ...simulationState, changeType } }; - }, -); - -/**** Updates ****/ +export const trainMailboxUpdated = async ( + callback = () => {}, + collectionName = "global", +) => { + const ref = firebaseInstance.db + .collection(collectionName) + .doc("train_mailbox"); + try { + await ref.onSnapshot((snapshot) => callback(snapshot.data())); + } catch (error) { + console.error(error); + } +}; /** * ----------------- - * updateSelectedPattern + * proposalUpdated * ----------------- */ -export const updateSelectedPattern = createAsyncThunk( - "updateSelectedPattern", - async (pattern) => { - const ref = firebaseInstance.db.collection("global").doc("world"); - const selectedPattern = await ref.update({ pattern_slug: pattern?.pattern_slug }); - console.log("Pattern selection updated."); - - return { selectedPattern: pattern }; - }); - +export const proposalUpdated = async ( + callback = () => {}, + collectionName = "global", +) => { + const ref = firebaseInstance.db.collection(collectionName).doc("proposal"); + try { + await ref.onSnapshot((snapshot) => callback(snapshot.data())); + } catch (error) { + console.error(error); + } +}; -/**** Listeners ****/ /** * ----------------- - * worldStateUpdated + * trainUpdated * ----------------- */ -export const worldStateUpdated = async (dispatch) => { - const ref = firebaseInstance.db.collection("global"); - - await ref.onSnapshot((snapshot) => { - snapshot.docChanges().forEach((change) => { - dispatch?.(getInitialWorldState()); - }); - }); +export const trainUpdated = async ( + callback = () => {}, + collectionName = "global", +) => { + const ref = firebaseInstance.db.collection(collectionName).doc("train"); + try { + await ref.onSnapshot((snapshot) => callback(snapshot.data())); + } catch (error) { + console.error(error); + } }; /** * ----------------- - * worldSimulationStateUpdated + * cargoUpdated * ----------------- */ -export const worldSimulationStateUpdated = async (dispatch) => { - const ref = firebaseInstance.db.collection("global_simulation"); - const patternRef = firebaseInstance.db.collection("patterns"); +export const cargoUpdated = async ( + callback = () => {}, + collectionName = "global", +) => { + const ref = firebaseInstance.db.collection(collectionName).doc("cargo"); + try { + await ref.onSnapshot((snapshot) => callback(snapshot.data())); + } catch (error) { + console.error(error); + } +}; - await ref.onSnapshot((snapshot) => { - snapshot.docChanges().forEach((change) => { - dispatch?.(getWorldSimulation(change.type)); - }); - }); +/** + * ----------------- + * signalsUpdated + * ----------------- + */ +export const signalsUpdated = async ( + callback = () => {}, + collectionName = "global", +) => { + const ref = firebaseInstance.db.collection(collectionName).doc("signals"); + try { + await ref.onSnapshot((snapshot) => callback(snapshot.data())); + } catch (error) { + console.error(error); + } }; +/** + * ----------------- + * updateInputMailbox + * ----------------- + */ +export const updateInputMailbox = async (eventString) => { + const ref = firebaseInstance.db.collection("global").doc("input_mailbox"); + try { + await ref.update({ input: eventString }); + } catch (error) { + console.error(error); + } +}; diff --git a/other/train-to-cloud-city/app/client/src/actions/trainActions.js b/other/train-to-cloud-city/app/client/src/actions/trainActions.js deleted file mode 100644 index 91e033a0..00000000 --- a/other/train-to-cloud-city/app/client/src/actions/trainActions.js +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2024 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { createAsyncThunk } from "@reduxjs/toolkit"; - -export const DELETE_CAR = "DELETE_CAR"; -export const ADD_CAR = "ADD_CAR"; - -const testFetch = createAsyncThunk("testFetch", async () => { - // Do we want to keep this in bigquery or add it to browser cache if - // we're trying to keep the logic lean - const headers = { - method: "GET", - headers: { - "Content-Type": "application/json", - "Access-Control-Allow-Origin": "*", - }, - }; - - let execution; - - try { - const response = await fetch( - `http://127.0.0.1:4000/createAttemptWorkflow`, - { mode: "cors" }, - headers, - ); - execution = await response.json(); - } catch (e) { - console.error(e); - } - - return execution; -}); - -/** - * ----------------- - * addCar - * ----------------- - * Adds a new "train coach" by taking in a "serviceId" that maps - * to an accompanying cloud workflow control statement(s) - */ -// TODO: serviceId is currently placeholder / the service shape still tbd -const addCar = (serviceId) => { - return { - type: ADD_CAR, - payload: { serviceId }, - }; -}; - -/** - * ----------------- - * deleteCar - * ----------------- - * Removes a "train coach" by taking in a "serviceId" that maps - * to an accompanying cloud workflow control statement(s) to delete - * from train - */ -// TODO: serviceId is currently placeholder / the service shape still tbd -const deleteCar = (serviceId) => { - return { - type: DELETE_CAR, - payload: { serviceId }, - }; -}; - -export { addCar, deleteCar, testFetch }; diff --git a/other/train-to-cloud-city/app/client/src/assets/Station.png b/other/train-to-cloud-city/app/client/src/assets/Station.png new file mode 100644 index 00000000..60e38131 Binary files /dev/null and b/other/train-to-cloud-city/app/client/src/assets/Station.png differ diff --git a/other/train-to-cloud-city/app/client/src/assets/banner.svg b/other/train-to-cloud-city/app/client/src/assets/banner.svg new file mode 100644 index 00000000..078edc27 --- /dev/null +++ b/other/train-to-cloud-city/app/client/src/assets/banner.svg @@ -0,0 +1,934 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/other/train-to-cloud-city/app/client/src/assets/conductor-success.gif b/other/train-to-cloud-city/app/client/src/assets/conductor-success.gif new file mode 100644 index 00000000..ad4c97a0 Binary files /dev/null and b/other/train-to-cloud-city/app/client/src/assets/conductor-success.gif differ diff --git a/other/train-to-cloud-city/app/client/src/assets/conductor-try-again.gif b/other/train-to-cloud-city/app/client/src/assets/conductor-try-again.gif new file mode 100644 index 00000000..ad4c97a0 Binary files /dev/null and b/other/train-to-cloud-city/app/client/src/assets/conductor-try-again.gif differ diff --git a/other/train-to-cloud-city/app/client/src/assets/conductor-wave.gif b/other/train-to-cloud-city/app/client/src/assets/conductor-wave.gif new file mode 100644 index 00000000..ad4c97a0 Binary files /dev/null and b/other/train-to-cloud-city/app/client/src/assets/conductor-wave.gif differ diff --git a/other/train-to-cloud-city/app/client/src/assets/error-state.svg b/other/train-to-cloud-city/app/client/src/assets/error-state.svg new file mode 100644 index 00000000..bd14e145 --- /dev/null +++ b/other/train-to-cloud-city/app/client/src/assets/error-state.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/other/train-to-cloud-city/app/client/src/assets/red-coach.svg b/other/train-to-cloud-city/app/client/src/assets/red-coach.svg deleted file mode 100644 index 7c82de43..00000000 --- a/other/train-to-cloud-city/app/client/src/assets/red-coach.svg +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/other/train-to-cloud-city/app/client/src/assets/red-front.svg b/other/train-to-cloud-city/app/client/src/assets/red-front.svg deleted file mode 100644 index a41fb5fb..00000000 --- a/other/train-to-cloud-city/app/client/src/assets/red-front.svg +++ /dev/null @@ -1,73 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/other/train-to-cloud-city/app/client/src/assets/signal-green.svg b/other/train-to-cloud-city/app/client/src/assets/signal-green.svg new file mode 100644 index 00000000..9a22a6db --- /dev/null +++ b/other/train-to-cloud-city/app/client/src/assets/signal-green.svg @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/other/train-to-cloud-city/app/client/src/assets/signal-off.svg b/other/train-to-cloud-city/app/client/src/assets/signal-off.svg new file mode 100644 index 00000000..20a7d078 --- /dev/null +++ b/other/train-to-cloud-city/app/client/src/assets/signal-off.svg @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/other/train-to-cloud-city/app/client/src/assets/signal-red.svg b/other/train-to-cloud-city/app/client/src/assets/signal-red.svg new file mode 100644 index 00000000..6357408a --- /dev/null +++ b/other/train-to-cloud-city/app/client/src/assets/signal-red.svg @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/other/train-to-cloud-city/app/client/src/assets/signal-yellow.svg b/other/train-to-cloud-city/app/client/src/assets/signal-yellow.svg new file mode 100644 index 00000000..e4b8ac9f --- /dev/null +++ b/other/train-to-cloud-city/app/client/src/assets/signal-yellow.svg @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/other/train-to-cloud-city/app/client/src/assets/speech-bubble.svg b/other/train-to-cloud-city/app/client/src/assets/speech-bubble.svg new file mode 100644 index 00000000..93b02080 --- /dev/null +++ b/other/train-to-cloud-city/app/client/src/assets/speech-bubble.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/other/train-to-cloud-city/app/client/src/assets/station.svg b/other/train-to-cloud-city/app/client/src/assets/station.svg deleted file mode 100644 index 7f341a98..00000000 --- a/other/train-to-cloud-city/app/client/src/assets/station.svg +++ /dev/null @@ -1,140 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/other/train-to-cloud-city/app/client/src/assets/success-state.svg b/other/train-to-cloud-city/app/client/src/assets/success-state.svg new file mode 100644 index 00000000..6302a606 --- /dev/null +++ b/other/train-to-cloud-city/app/client/src/assets/success-state.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/other/train-to-cloud-city/app/client/src/assets/train-backdrop.png b/other/train-to-cloud-city/app/client/src/assets/train-backdrop.png new file mode 100644 index 00000000..b903f84d Binary files /dev/null and b/other/train-to-cloud-city/app/client/src/assets/train-backdrop.png differ diff --git a/other/train-to-cloud-city/app/client/src/assets/train-car.png b/other/train-to-cloud-city/app/client/src/assets/train-car.png new file mode 100644 index 00000000..5446f9fd Binary files /dev/null and b/other/train-to-cloud-city/app/client/src/assets/train-car.png differ diff --git a/other/train-to-cloud-city/app/client/src/assets/train-engine.png b/other/train-to-cloud-city/app/client/src/assets/train-engine.png new file mode 100644 index 00000000..d89a5c48 Binary files /dev/null and b/other/train-to-cloud-city/app/client/src/assets/train-engine.png differ diff --git a/other/train-to-cloud-city/app/client/src/components/CargoResult.jsx b/other/train-to-cloud-city/app/client/src/components/CargoResult.jsx new file mode 100644 index 00000000..1af6cae7 --- /dev/null +++ b/other/train-to-cloud-city/app/client/src/components/CargoResult.jsx @@ -0,0 +1,79 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React, { useState, useEffect } from "react"; +import { useSelector, useDispatch } from "react-redux"; +import { getPatterns } from "../actions/coreActions"; +import SuccessState from "../assets/conductor-success.gif"; +import TryAgainState from "../assets/conductor-try-again.gif"; +import ExtrasQRCode from "../assets/qrcode-extras.png"; +import "./styles/CargoResult.css"; + +/** + * CargoResult + * ----------------- + * + */ +const CargoResult = (props) => { + const { proposal } = props; + const { pattern_slug, proposal_result } = proposal; + const state = useSelector((state) => state); + const dispatch = useDispatch(); + + useEffect(() => { + dispatch(getPatterns()); + }, [dispatch]); + + const { patterns } = state.coreReducer; + const selectedPattern = (patterns?.filter(p => p.slug === pattern_slug))?.[0]; + + const showSuccess = proposal_result?.clear && proposal_result?.reason; + const showError = !proposal_result?.clear && proposal_result?.reason; + + + return selectedPattern?.checkpoints?.length === 0 ? ( +

{'No checkpoints available.'}

+ ) : ( +
+

Goal: {selectedPattern?.description}

+ {selectedPattern?.checkpoints?.map((step, index) => ( +

+ {`Step ${index + 1}: `} + {step.description} +

+ ))} + {showError && ( +
+

{"Oh no!"}

+
+ try again +
+

{proposal_result?.reason}

+
+ )} + {showSuccess && ( +
+
+ success +
+
{"Huzzah!"}
+
{proposal_result?.reason}
+ Extras +
+ )} +
+ ); +}; + +export default CargoResult; diff --git a/other/train-to-cloud-city/app/client/src/components/ControlPanel.jsx b/other/train-to-cloud-city/app/client/src/components/ControlPanel.jsx index 33817764..2071bb0e 100644 --- a/other/train-to-cloud-city/app/client/src/components/ControlPanel.jsx +++ b/other/train-to-cloud-city/app/client/src/components/ControlPanel.jsx @@ -15,22 +15,36 @@ import React from "react"; import "./styles/ControlPanel.css"; -const ControlPanel = (worldState, proposal, result) => { +const ControlPanel = (props) => { + const { cargo, proposalResult, trainMailbox } = props; + return (
-
- World State: -

{JSON.stringify(worldState)}

+
+

Current Train Cloud Cargo:

+ {cargo?.actual_cargo.length ? ( +

{JSON.stringify(cargo?.actual_cargo)}

+ ) : ( + "Waiting for cargo to be loaded..." + )}
-
- Proposal: -

{JSON.stringify(proposal)}

+
+

Proposal Result:

+ {proposalResult ? ( +

{JSON.stringify(proposalResult?.reason)}

+ ) : ( + "Waiting to process cargo ..." + )}
-
- Proposal Result: -

{JSON.stringify(result)}

+
+

Train:

+ {trainMailbox ? ( +

{trainMailbox?.input}

+ ) : ( + "Waiting for train events ..." + )}
diff --git a/other/train-to-cloud-city/app/client/src/components/Dashboard.jsx b/other/train-to-cloud-city/app/client/src/components/Dashboard.jsx index 9ee72379..7fc88275 100644 --- a/other/train-to-cloud-city/app/client/src/components/Dashboard.jsx +++ b/other/train-to-cloud-city/app/client/src/components/Dashboard.jsx @@ -13,11 +13,13 @@ // limitations under the License. import React from "react"; -import { useSelector } from "react-redux"; +import { useSelector, useDispatch } from "react-redux"; import ControlPanel from "./ControlPanel"; -import QuizForm from "./QuizForm"; +import CargoResult from "./CargoResult"; import Train from "./Train"; import Signal from "./Signal"; +import Ribbon from "./Ribbon"; +import { stopMission, updateInputMailbox } from "../actions/coreActions"; import "./styles/Dashboard.css"; /** @@ -27,47 +29,63 @@ import "./styles/Dashboard.css"; */ const Dashboard = (props) => { const state = useSelector((state) => state); - const { isSimulator } = props; - const { services, selectedPattern, worldState } = state.coreReducer; - const { signals, train, proposal, proposal_result } = worldState || {}; + const dispatch = useDispatch(); + const { signals, cargo, train, proposal, trainMailbox } = props || {}; + const { patterns, services, worldState } = state.coreReducer; + + // Stop and reset whole mission + const handleStopMission = async (event) => { + dispatch(stopMission()); + await updateInputMailbox("reset"); + window.location.replace("/"); + }; return (
- -
- - - - - +
+

{`Your Mission: ${proposal?.pattern_slug}`}

+
+ +
+
+
+
+

Loaded cargo ...

+ + + + + +
+
- {!isSimulator && ( -
-

{`Your Mission: ${selectedPattern?.name}`}

- {selectedPattern && } -
- )}
+
+ +
+
); }; diff --git a/other/train-to-cloud-city/app/client/src/components/Header.jsx b/other/train-to-cloud-city/app/client/src/components/Header.jsx index 165edf3f..4a05e634 100644 --- a/other/train-to-cloud-city/app/client/src/components/Header.jsx +++ b/other/train-to-cloud-city/app/client/src/components/Header.jsx @@ -13,6 +13,7 @@ // limitations under the License. import React from "react"; +import Ribbon from "./Ribbon"; import "./styles/Header.css"; /** @@ -23,15 +24,10 @@ import "./styles/Header.css"; const Header = () => { return (
-

🚂 Train to Cloud City 🛤️

-
- Welcome stranger! Help us explore Cloud City and should you choose - to join us, we promise a wealth of knowledge at the end of this - mission. Begin your quest by selecting a mission below. -
+
+
); }; export default Header; - diff --git a/other/train-to-cloud-city/app/client/src/components/Main.jsx b/other/train-to-cloud-city/app/client/src/components/Main.jsx index 3669e10a..8c9c76eb 100644 --- a/other/train-to-cloud-city/app/client/src/components/Main.jsx +++ b/other/train-to-cloud-city/app/client/src/components/Main.jsx @@ -15,78 +15,108 @@ import React, { useEffect, useState } from "react"; import { useSelector, useDispatch } from "react-redux"; import Dashboard from "./Dashboard"; -import ToggleButton from "./ToggleButton"; import { - getWorld, + getWorldState, getPatterns, getServices, + signalsUpdated, + cargoUpdated, + trainUpdated, + trainMailboxUpdated, + updateInputMailbox, + proposalUpdated, } from "../actions/coreActions"; +import ConductorWave from "../assets/conductor-wave.gif"; import "./styles/Main.css"; /** * Main * ----------------- - * + * Sets environment and data to populate into dashboard + * which is in 3 different states (simulator, realtime, virtual input) */ const Main = (props) => { const state = useSelector((state) => state); const dispatch = useDispatch(); + const [toggled, setToggle] = useState(false); const [simulator, setSimulator] = useState(false); - useEffect(() => { - async function fetchData() { - await Promise.all([ - dispatch(getServices()), - dispatch(getPatterns()) - ]); - }; - fetchData(); - }, [dispatch]); + const [signals, setSignals] = useState({}); + const [cargo, setCargo] = useState({}); + const [train, setTrain] = useState({}); + const [pattern, setPattern] = useState({}); + const [proposal, setProposal] = useState({}); + const [trainMailbox, setTrainMailbox] = useState({}); - // Turn on simulator const handleSimulator = async (event) => { - setToggle(event.target.checked); - setSimulator(event.target.checked); - await getWorld({ - dispatch, - isSimulator: event.target.checked, - }); + setToggle(!simulator); + setSimulator(!simulator); + dispatch?.(getWorldState(simulator ? "global_simulation" : "global")); }; - // Manually select pattern const handlePatternSelect = async (event, pattern) => { setToggle(true); - await getWorld({ pattern, dispatch }); + setPattern(pattern); + dispatch?.(getWorldState(simulator ? "global_simulation" : "global")); + }; + + const cleanSlate = async () => { + try { + await updateInputMailbox("reset"); + Promise.all([dispatch(getServices()), dispatch(getPatterns())]); + } catch (error) { + console.log(error); + } }; + useEffect(() => { + const collection = simulator ? "global_simulation" : "global"; + + cleanSlate(); + // Listen for when patterns are updated + proposalUpdated((data) => { + setToggle(!!data.pattern_slug); + if (!!data.pattern_slug) { + setProposal(data); + } else { + setProposal({}); + cleanSlate(); + } + }, collection); + signalsUpdated((data) => setSignals(data), collection); + trainUpdated((data) => setTrain(data), collection); + cargoUpdated((data) => setCargo(data), collection); + trainMailboxUpdated((data) => setTrainMailbox(data), collection); + }, [simulator]); + return (
- {toggled ? ( - - ) : ( -
-

Choose your adventure

-
- + {proposal && !proposal.pattern_slug && ( +
+
+ welcome-wave
-
- {state.coreReducer.patterns?.map((p, index) => ( - - ))} +
+

Choose your adventure

+
+ {state.coreReducer.patterns?.map((p, index) => ( + + ))} +
)} + {toggled + ? + : {'Turn on simulator'}}
); diff --git a/other/train-to-cloud-city/app/client/src/components/QuizForm.jsx b/other/train-to-cloud-city/app/client/src/components/QuizForm.jsx deleted file mode 100644 index 5e4a59e4..00000000 --- a/other/train-to-cloud-city/app/client/src/components/QuizForm.jsx +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright 2024 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import React, { useState, useEffect } from "react"; -import { useSelector, useDispatch } from "react-redux"; -import { required, composeValidators, validateServices } from "../helpers/formValidation"; -import { Form, Field } from "react-final-form"; -import { addCar, deleteCar } from "../actions/trainActions"; -import { - getServices, -} from "../actions/coreActions"; -import ExtrasQRCode from "../assets/qrcode-extras.png"; -import "./styles/QuizForm.css"; - -/** - * QuizForm - * ----------------- - * - */ -const QuizForm = (props) => { - const { selectedPattern } = props; - const state = useSelector((state) => state); - const dispatch = useDispatch(); - - const [errorMessage, setErrorMessage] = useState({ message: '', result: {} }); - const [successMessage, setSuccessMessage] = useState({ message: '', result: {} }); - - useEffect(() => { - async function fetchData() { - await dispatch(getServices()); - }; - fetchData(); - }, [dispatch]); - - const services = state.coreReducer.services; - - const onSubmit = async (results) => { - const proposal = { - service_slugs: Object.keys(results).map((k) => results[k]) - }; - - let result, error; - - try { - // TODO: pass through current pattern/mission and proposal - result = await validateServices(selectedPattern, proposal); - setSuccessMessage(result); - setErrorMessage(); // reset - // TODO: pass through current pattern/mission and proposal - //values.map((service) => dispatch(addCar(service))); - } catch (error) { - setSuccessMessage(); // reset - setErrorMessage(error); - } - }; - - const onReset = async (formReset) => { - formReset(); - dispatch(deleteCar()); - }; - - return selectedPattern?.checkpoints?.length === 0 ? ( -
No checkpoints available.
- ) : ( -
( - -

Goal: {selectedPattern?.description}

- {selectedPattern?.checkpoints?.map((step, index) => ( - - {({ input, meta }) => ( -
-
- {`Step ${index + 1}: ${step.description}`} -
-
- - - {meta.error && meta.touched && ( - {meta.error} - )} -
-
- )} -
- ))} -
- - -
- {(submitting || errorMessage?.message || successMessage?.message) && ( -
- {submitting && 'Calculating responses ....'} - {errorMessage?.message && ( -
-

{'Oh no!'}

-

{errorMessage.message}

-
- )} - {successMessage?.message && ( -
-

{'Huzzah!'}

-

{successMessage.message}

- Extras -
- )} -
- )} -
- )} - /> - ); -}; - -export default QuizForm; diff --git a/other/train-to-cloud-city/app/client/src/reducers/trainReducer.js b/other/train-to-cloud-city/app/client/src/components/Ribbon.jsx similarity index 55% rename from other/train-to-cloud-city/app/client/src/reducers/trainReducer.js rename to other/train-to-cloud-city/app/client/src/components/Ribbon.jsx index 0ea8d4ce..8b69bb76 100644 --- a/other/train-to-cloud-city/app/client/src/reducers/trainReducer.js +++ b/other/train-to-cloud-city/app/client/src/components/Ribbon.jsx @@ -12,28 +12,25 @@ // See the License for the specific language governing permissions and // limitations under the License. -export const DELETE_CAR = "DELETE_CAR"; -export const ADD_CAR = "ADD_CAR"; +import React from "react"; +import "./styles/Ribbon.css"; -// Default train state -const initialState = { - numOfCars: 0, +/** + * Ribbon + * ----------------- + * + */ +const Ribbon = () => { + return ( +
+
+
+
+
+
+
+
+ ); }; -export const trainReducer = (state = initialState, action) => { - switch (action.type) { - case ADD_CAR: { - return { - ...state, - numOfCars: state.numOfCars + 1, - }; - } - case DELETE_CAR: - return { - ...state, - numOfCars: state.numOfCars - 1, - }; - default: - return state; - } -}; +export default Ribbon; diff --git a/other/train-to-cloud-city/app/client/src/components/Signal.jsx b/other/train-to-cloud-city/app/client/src/components/Signal.jsx index e64fe88f..9e867aa8 100644 --- a/other/train-to-cloud-city/app/client/src/components/Signal.jsx +++ b/other/train-to-cloud-city/app/client/src/components/Signal.jsx @@ -13,7 +13,11 @@ // limitations under the License. import React from "react"; -import Station from "../assets/station.svg"; +import Station from "../assets/station.png"; +import SignalGreen from "../assets/signal-green.svg"; +import SignalOff from "../assets/signal-off.svg"; +import SignalYellow from "../assets/signal-yellow.svg"; +import SignalRed from "../assets/signal-red.svg"; import "./styles/Signal.css"; /** @@ -31,33 +35,27 @@ const Signal = (props) => { ? checkpointClasses.concat(" here") : checkpointClasses; - const redLightClasses = - actual_state === "stop" - ? "light red on" - : target_state === "stop" - ? "light red blinking" - : "light red"; - const greenLightClasses = - actual_state === "clear" - ? "light green on" - : target_state === "clear" - ? "light green blinking" - : "light green"; + let signalLight; + switch (target_state) { + case "stop": { + signalLight = SignalRed; + break; + } + case "clear": { + signalLight = SignalGreen; + break; + } + default: { + signalLight = SignalOff; + } + } return (
+
+ {isStation ? Station : signalLight} +
- {isStation ? ( -
- Station -
- ) : ( -
-
-
-
-
- )}
); }; diff --git a/other/train-to-cloud-city/app/client/src/components/ToggleButton.jsx b/other/train-to-cloud-city/app/client/src/components/ToggleButton.jsx deleted file mode 100644 index d8ed7c54..00000000 --- a/other/train-to-cloud-city/app/client/src/components/ToggleButton.jsx +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright 2024 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import React, { useState } from "react"; -import "./styles/ToggleButton.css"; - -/** - * ToggleButton - * ----------------- - * - */ -const ToggleButton = (props) => { - const { label, onChange } = props; - const [toggled, setToggle] = useState(false); - - const handleChange = (event) => { - setToggle(event.target.checked); - onChange?.(event); - }; - - return ( -
- {label}{" "} -
- - -
-
- ); -}; - -export default ToggleButton; diff --git a/other/train-to-cloud-city/app/client/src/components/Train.jsx b/other/train-to-cloud-city/app/client/src/components/Train.jsx index 595fec57..c9744d5c 100644 --- a/other/train-to-cloud-city/app/client/src/components/Train.jsx +++ b/other/train-to-cloud-city/app/client/src/components/Train.jsx @@ -23,30 +23,14 @@ import "./styles/Train.css"; * */ const Train = (props) => { - const { train } = props; - const { actual_cargo , reader, target_location, actual_location } = train || {}; - - const state = useSelector((state) => state); - const dispatch = useDispatch(); - - // TODO: revisit when global_simulation actual_cargo is updated - let cargo = actual_cargo || []; - - if(typeof actual_cargo === "string") { - cargo = actual_cargo?.split(','); - } - - let cars = []; - cargo.forEach((cargo, index) => cars.push()); - - const trainClasses = "train".concat(` ${actual_location}`); - + const { train, cargo } = props; return (
+
-
- {cars?.map(c => c)} +
+ {cargo?.actual_cargo?.map((c, index) => )}
diff --git a/other/train-to-cloud-city/app/client/src/components/TrainCoach.jsx b/other/train-to-cloud-city/app/client/src/components/TrainCoach.jsx index 1312e940..fb3be372 100644 --- a/other/train-to-cloud-city/app/client/src/components/TrainCoach.jsx +++ b/other/train-to-cloud-city/app/client/src/components/TrainCoach.jsx @@ -13,8 +13,8 @@ // limitations under the License. import React from "react"; -import Car from "../assets/red-coach.svg"; -import FrontCar from "../assets/red-front.svg"; +import Car from "../assets/train-car.png"; +import FrontCar from "../assets/train-engine.png"; import "./styles/TrainCoach.css"; /** @@ -23,13 +23,17 @@ import "./styles/TrainCoach.css"; * */ const TrainCoach = (props) => { - const { name } = props; + const { name, cargo } = props; return (
- {name === "front" ? FrontCar : Car} + {name === "front" ? ( + FrontCar + ) : ( + Car + )}
diff --git a/other/train-to-cloud-city/app/client/src/components/styles/QuizForm.css b/other/train-to-cloud-city/app/client/src/components/styles/CargoResult.css similarity index 78% rename from other/train-to-cloud-city/app/client/src/components/styles/QuizForm.css rename to other/train-to-cloud-city/app/client/src/components/styles/CargoResult.css index def814ff..a3ad455e 100644 --- a/other/train-to-cloud-city/app/client/src/components/styles/QuizForm.css +++ b/other/train-to-cloud-city/app/client/src/components/styles/CargoResult.css @@ -18,22 +18,29 @@ display: flex; align-items: center; flex-direction: column; - font-size: 12px; - font-style: italic; + font-size: 16px; text-align: center; border: 1px solid black; padding: 10px; background: var(--lilyWhite); color: var(--melanzane); + margin-bottom: 10px; } form#missionForm, form#missionForm .formMissionInput { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; margin-top: 10px; } form#missionForm select { - margin: 10px auto; + font-family: var(--font); + font-size: 16px; + max-width: 90%; + padding: 10px auto; } form#missionForm .buttons { @@ -47,19 +54,27 @@ form#missionForm .buttons { form#missionForm .formError { color: red; display: block; - font-size: 10px; + font-size: 12px; } img.qrcode { height: auto; width: 200px; + margin: 15px; } .resultContainer { display: flex; align-items: center; justify-content: center; - border: 1px solid black; + flex-direction: column; padding: 10px; - background: white; + width: inherit; } + +.tryAgainState img, +.successState img { + width: 150px; + margin: 25px; +} + diff --git a/other/train-to-cloud-city/app/client/src/components/styles/ControlPanel.css b/other/train-to-cloud-city/app/client/src/components/styles/ControlPanel.css index f5e35827..efd630bc 100644 --- a/other/train-to-cloud-city/app/client/src/components/styles/ControlPanel.css +++ b/other/train-to-cloud-city/app/client/src/components/styles/ControlPanel.css @@ -16,17 +16,20 @@ .controlPanelContainer { position: relative; - margin: 10px; + width: inherit; + margin: 15px auto; + font-family: var(--font); + font-size: 20px; } .controlPanel { - font-family: monospace; - font-size: 12px; - width: inherit; - height: 200px; + display: flex; + align-items: center; + justify-content: space-evenly; + flex-direction: column; + color: white; padding: 10px; border-radius: 10px; background: black; - color: white; overflow-y: scroll; } diff --git a/other/train-to-cloud-city/app/client/src/components/styles/Dashboard.css b/other/train-to-cloud-city/app/client/src/components/styles/Dashboard.css index 9744e6c9..fe1ebcb2 100644 --- a/other/train-to-cloud-city/app/client/src/components/styles/Dashboard.css +++ b/other/train-to-cloud-city/app/client/src/components/styles/Dashboard.css @@ -16,14 +16,22 @@ .dashboardContainer { position: relative; - margin: 10px; color: var(--melanzane); + font-size: larger; } .dashboardWrapper { display: flex; align-items: center; - justify-content: space-evenly; + justify-content: center; + margin: 20px auto; +} + +.dashboardSignals { + display: flex; + align-items: center; + justify-content: flex-end; + width: 100%; } .dashboardPanel { @@ -31,19 +39,33 @@ position: relative; flex-direction: column; margin: auto 10px; - max-width: 400px; + max-width: 500px; min-height: 500px; width: 100%; - height: 100%; - background: var(--lilyWhite); - border: 3px solid var(--lemonGrass); + height: 700px; + background: white; + border: 3px solid var(--melanzane); padding: 15px; - overflow: hidden; + overflow-y: scroll; } .dashboardWrapper .columns { display: flex; flex-direction: row; + align-items: center; justify-content: space-between; + width: 100%; overflow: hidden; } + +.dashboardContainer .actionPanel { + margin: 20px; +} + +.dashboardContainer .actionPanel button.stop { + background: var(--cinnabar); +} + +.dashboardContainer .actionPanel button.reset { + background: var(--dodgerblue); +} diff --git a/other/train-to-cloud-city/app/client/src/components/styles/Header.css b/other/train-to-cloud-city/app/client/src/components/styles/Header.css index 42d4fe49..1d15e4cc 100644 --- a/other/train-to-cloud-city/app/client/src/components/styles/Header.css +++ b/other/train-to-cloud-city/app/client/src/components/styles/Header.css @@ -15,21 +15,19 @@ */ .headerContainer { - display: flex; - align-items: center; - justify-content: center; + display: block; text-align: center; color: var(--melanzane); - background: var(--lilyWhite); - border: 5px solid var(--lemonGrass); - border-inline: 7px solid var(--lilyWhite); - flex-direction: column; + width: 100%; } -.headerContent { +.headerBanner { display: flex; align-items: center; justify-content: center; - max-width: 500px; - padding: 10px; + min-height: 188px; + background-image: url(../../assets/banner.svg); + background-size: cover; + background-repeat: no-repeat; + background-position: center; } diff --git a/other/train-to-cloud-city/app/client/src/components/styles/Main.css b/other/train-to-cloud-city/app/client/src/components/styles/Main.css index e21af631..332e9a01 100644 --- a/other/train-to-cloud-city/app/client/src/components/styles/Main.css +++ b/other/train-to-cloud-city/app/client/src/components/styles/Main.css @@ -17,24 +17,30 @@ .mainContainer { display: flex; align-items: center; - justify-content: center; + justify-content: flex-start; text-align: center; flex-direction: column; margin: auto; + font-family: var(--font); + font-size: larger; +} + +.mainContainer a { + font-size: 10px; } .mainWrapper { width: 100%; } +.welcomeImage img { + width: 250px; + margin: 25px; +} + .row { margin: 10px; display: flex; align-items: center; justify-content: center; } - -.row button { - padding: 10px; - margin: 5px; -} diff --git a/other/train-to-cloud-city/app/client/src/components/styles/Ribbon.css b/other/train-to-cloud-city/app/client/src/components/styles/Ribbon.css new file mode 100644 index 00000000..1f93b120 --- /dev/null +++ b/other/train-to-cloud-city/app/client/src/components/styles/Ribbon.css @@ -0,0 +1,46 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.ribbonContainer { + display: block; + text-align: center; + color: var(--melanzane); + width: 100%; +} + +.ribbon { + width: 100%; +} + +.ribbon .red, +.ribbon .blue, +.ribbon .yellow, +.ribbon .green { + width: 100%; + height: 5px; +} +.ribbon .red { + background-color: var(--cinnabar); +} +.ribbon .blue { + background-color: var(--dodgerblue); +} +.ribbon .yellow { + background-color: var(--selectiveyellow); +} +.ribbon .green { + background-color: var(--eucalyptus); +} diff --git a/other/train-to-cloud-city/app/client/src/components/styles/Signal.css b/other/train-to-cloud-city/app/client/src/components/styles/Signal.css index 94067a36..52f038aa 100644 --- a/other/train-to-cloud-city/app/client/src/components/styles/Signal.css +++ b/other/train-to-cloud-city/app/client/src/components/styles/Signal.css @@ -19,20 +19,19 @@ flex-direction: column; align-items: center; margin-top: 10px; + margin-bottom: 10px; + margin-left: 15px; + margin-right: 15px; } .signal { - position: relative; - height: 50px; - width: 20px; - border: 3px solid black; - background: black; - border-radius: 5px; display: flex; - flex-direction: column; align-items: center; - justify-content: space-evenly; - margin: 10px; +} + +.signal img { + height: 70px; + width: auto; } .stationCheckpoint { @@ -40,16 +39,12 @@ margin: 3px; } -.stationCheckpoint img { - width: 50px; - height: auto; -} - .circle { - width: 20px; - height: 20px; + width: 10px; + height: 10px; background-color: black; border-radius: 50px; + margin-top: 5px; } .circle.here { @@ -59,13 +54,13 @@ .connector:before { content: ""; + display: block; position: relative; - width: 71px; - height: 2px; + width: 95px; + height: 1px; background-color: black; - top: 10px; - display: block; - right: -20px; + top: 5px; + left: 9px; } .light { diff --git a/other/train-to-cloud-city/app/client/src/components/styles/ToggleButton.css b/other/train-to-cloud-city/app/client/src/components/styles/ToggleButton.css deleted file mode 100644 index 2e724d7e..00000000 --- a/other/train-to-cloud-city/app/client/src/components/styles/ToggleButton.css +++ /dev/null @@ -1,85 +0,0 @@ -/** - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -.toggleContainer { - text-align: center; -} -.toggleWrapper { - position: relative; - width: 75px; - display: inline-block; - text-align: left; - top: 8px; -} -.checkbox { - display: none; -} -.label { - display: block; - overflow: hidden; - cursor: pointer; - border: 0 solid #bbb; - border-radius: 20px; -} -.inner { - display: block; - width: 200%; - margin-left: -100%; - transition: margin 0.3s ease-in 0s; -} -.inner:before, -.inner:after { - float: left; - width: 50%; - height: 36px; - padding: 0; - line-height: 36px; - color: #fff; - font-weight: bold; - box-sizing: border-box; -} -.inner:before { - content: "YES"; - padding-left: 10px; - background-color: #060; - color: #fff; -} -.inner:after { - content: "NO"; - padding-right: 10px; - background-color: #bbb; - color: #fff; - text-align: right; -} -.switch { - display: block; - width: 24px; - margin: 5px; - background: #fff; - position: absolute; - top: 0; - bottom: 0; - right: 40px; - border: 0 solid #bbb; - border-radius: 20px; - transition: all 0.3s ease-in 0s; -} -.checkbox:checked + .label .inner { - margin-left: 0; -} -.checkbox:checked + .label .switch { - right: 0px; -} diff --git a/other/train-to-cloud-city/app/client/src/components/styles/Train.css b/other/train-to-cloud-city/app/client/src/components/styles/Train.css index 05f288e4..4910ee14 100644 --- a/other/train-to-cloud-city/app/client/src/components/styles/Train.css +++ b/other/train-to-cloud-city/app/client/src/components/styles/Train.css @@ -16,12 +16,12 @@ .trainContainer { position: relative; -} - -.cloudTrain { display: flex; align-items: center; justify-content: center; + width: 100%; + background-image: url(../../assets/train-backdrop.png); + background-size: cover; } .container { @@ -31,10 +31,8 @@ .content { position: relative; - height: 100px; overflow: hidden; border: 5px solid var(--melanzane); - background: var(--lemonGrass); } .track { @@ -42,6 +40,7 @@ width: 200em; height: 5px; background-color: #333; + bottom: 30px; } .track:before { @@ -98,30 +97,35 @@ .train { position: relative; width: 50px; - height: 70px; + height: 150px; display: flex; justify-content: flex-end; align-items: flex-end; - z-index: 1; - top: 7px; + z-index: 2; + bottom: 22px; } +.train.mission_check, .train.station { - left: 0%; + left: 35%; } -.train.one { - left: 25%; +.train.one, +.train.checkpoint_1 { + left: 47%; } -.train.two { - left: 45%; +.train.two, +.train.checkpoint_2 { + left: 60%; } -.train.three { - left: 65%; +.train.three, +.train.checkpoint_3 { + left: 70%; } -.train.four { +.train.four, +.train.checkpoint_4 { left: 85%; } diff --git a/other/train-to-cloud-city/app/client/src/helpers/formValidation.js b/other/train-to-cloud-city/app/client/src/helpers/formValidation.js deleted file mode 100644 index 14005acc..00000000 --- a/other/train-to-cloud-city/app/client/src/helpers/formValidation.js +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2024 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -const required = (value) => (value ? undefined : "Required"); -const composeValidators = - (...validators) => - (value) => - validators.reduce( - (error, validator) => error || validator(value), - undefined, - ); - -/** - * validateServices - * -------------------------- - * TODO: Call external service validator api to ensure listed services - * given are valid for the pattern. - */ -const validateServices = async (pattern, proposal) => { - const { checkpoints } = pattern; - const { service_slugs } = proposal; - - let matchedCheckpoints = []; - let unmatchedCheckpoints = []; - - checkpoints?.map((c, index) => { - // Check if any are found, push to list - // * = any service can fit - const matchedServices = c?.satisfying_services?.filter(s => s === '*' || service_slugs.includes(s)); - matchedServices.length ? matchedCheckpoints.push(index) : unmatchedCheckpoints.push(index); - }); - - const result = { - proposal, - pattern, - unmatchedCheckpoints, - matchedCheckpoints - }; - - if(unmatchedCheckpoints.length) { - throw new Error("Patterns between proposal and mission do not match", result); - return; - } - - return Promise.resolve({ message: 'Services match!', result }); -} - -export { validateServices, required, composeValidators }; diff --git a/other/train-to-cloud-city/app/client/src/reducers/coreReducer.js b/other/train-to-cloud-city/app/client/src/reducers/coreReducer.js index 0bb37401..160f6ecf 100644 --- a/other/train-to-cloud-city/app/client/src/reducers/coreReducer.js +++ b/other/train-to-cloud-city/app/client/src/reducers/coreReducer.js @@ -14,20 +14,15 @@ import { createSlice } from "@reduxjs/toolkit"; import { - getInitialWorldState, - getWorldSimulation, + getWorldState, getServices, getPatterns, - selectPattern, - updateSelectedPattern } from "../actions/coreActions"; const initialState = { - selectedPattern: {}, services: [], patterns: [], worldState: [], - simulationState: [], }; const coreSlice = createSlice({ @@ -38,14 +33,10 @@ const coreSlice = createSlice({ // Note: intentionally using same property worldState for // both actual and simulated world state (doesn't matter for web app) // They just need to have one source of truth. - .addCase(getInitialWorldState.fulfilled, (state, action) => { + .addCase(getWorldState.fulfilled, (state, action) => { state.worldState = action?.payload?.state; return state; }) - .addCase(getWorldSimulation.fulfilled, (state, action) => { - state.worldState = action?.payload?.simulationState; - return state; - }) .addCase(getServices.fulfilled, (state, action) => { state.services = action?.payload?.services; return state; @@ -54,11 +45,6 @@ const coreSlice = createSlice({ state.patterns = action?.payload?.patterns; return state; }) - .addCase(updateSelectedPattern.fulfilled, (state, action) => { - const pattern = action?.payload?.selectedPattern; - state.selectedPattern = pattern; - return state; - }) .addDefaultCase((state, action) => initialState); }, }); diff --git a/other/train-to-cloud-city/app/client/src/store.js b/other/train-to-cloud-city/app/client/src/store.js index 1cbc5c6c..e7356822 100644 --- a/other/train-to-cloud-city/app/client/src/store.js +++ b/other/train-to-cloud-city/app/client/src/store.js @@ -13,12 +13,10 @@ // limitations under the License. import { configureStore } from "@reduxjs/toolkit"; -import { trainReducer } from "./reducers/trainReducer"; import coreReducer from "./reducers/coreReducer"; const store = configureStore({ reducer: { - trainReducer, coreReducer, }, middleware: (getDefaultMiddleware) => getDefaultMiddleware(), diff --git a/other/train-to-cloud-city/app/server/README.md b/other/train-to-cloud-city/app/server/README.md index 8caf1699..0bc30fd1 100644 --- a/other/train-to-cloud-city/app/server/README.md +++ b/other/train-to-cloud-city/app/server/README.md @@ -1,7 +1,7 @@ - ## setup notes Install modules (TODO: setup requirements.txt, etc) + ``` pip install fastapi pip install "uvicorn[standard]" @@ -9,6 +9,7 @@ pip install firestore ``` Run dev server locally + ``` uvicorn main:app --reload -``` \ No newline at end of file +``` diff --git a/other/train-to-cloud-city/app/server/static/services.json b/other/train-to-cloud-city/app/server/static/services.json index 3b0191ad..15ec2190 100644 --- a/other/train-to-cloud-city/app/server/static/services.json +++ b/other/train-to-cloud-city/app/server/static/services.json @@ -1,1490 +1,1490 @@ { - "duet-ai": { - "slug": "duet-ai", - "name": "Duet AI in Google Cloud", - "four_words": "AI-powered collaborator", - "doc_url": "https://cloud.google.com/duet-ai" - }, - "container-analysis": { - "slug": "container-analysis", - "name": "Container Analysis", - "four_words": "Automated security scanning", - "doc_url": "https://cloud.google.com/container-analysis/docs/container-scanning-overview" - }, - "vertex-ai-workbench": { - "slug": "vertex-ai-workbench", - "name": "Vertex AI Workbench", - "four_words": "Jupyter-based environment for Data Science", - "doc_url": "https://cloud.google.com/vertex-ai/docs/workbench" - }, - "api-gateway": { - "slug": "api-gateway", - "name": "API Gateway", - "four_words": "Fully managed API Gateway", - "doc_url": "https://cloud.google.com/api-gateway/docs" - }, - "cloud-memorystore": { - "slug": "cloud-memorystore", - "name": "Cloud Memorystore", - "four_words": "Managed Redis and Memcached", - "doc_url": "https://cloud.google.com/memorystore/docs/" - }, - "ml-kit-for-firebase": { - "slug": "ml-kit-for-firebase", - "name": "ML Kit for Firebase", - "four_words": "ML APIs for mobile", - "doc_url": "https://firebase.google.com/products/ml" - }, - "web-security-scanner": { - "slug": "web-security-scanner", - "name": "Web Security Scanner", - "four_words": "Identifies web-app security vulnerabilities", - "doc_url": "https://cloud.google.com/security-command-center/docs/concepts-web-security-scanner-overview" - }, - "traffic-director": { - "slug": "traffic-director", - "name": "Traffic Director", - "four_words": "Service mesh traffic management", - "doc_url": "https://cloud.google.com/traffic-director/docs/" - }, - "migrate-from-teradata": { - "slug": "migrate-from-teradata", - "name": "Migrate from Teradata", - "four_words": "Migrate from Teradata to BigQuery", - "doc_url": "https://cloud.google.com/architecture/dw2bq/dw-bq-migration-overview" - }, - "access-transparency": { - "slug": "access-transparency", - "name": "Access Transparency", - "four_words": "Audit cloud provider access", - "doc_url": "https://cloud.google.com/cloud-provider-access-management/access-transparency/docs/overview" - }, - "maps-sdk-for-unity": { - "slug": "maps-sdk-for-unity", - "name": "Maps SDK for Unity", - "four_words": "Unity SDK for games", - "doc_url": "https://developers.google.com/maps/documentation" - }, - "access-context-manager": { - "slug": "access-context-manager", - "name": "Access Context Manager", - "four_words": "Fine-grained, attribute based access-control", - "doc_url": "https://cloud.google.com/access-context-manager/docs" - }, - "recaptcha-enterprise": { - "slug": "recaptcha-enterprise", - "name": "reCAPTCHA Enterprise", - "four_words": "Protect website against bots", - "doc_url": "https://cloud.google.com/recaptcha-enterprise/docs" - }, - "cloud-vision": { - "slug": "cloud-vision", - "name": "Cloud Vision", - "four_words": "Image recognition and classification", - "doc_url": "https://cloud.google.com/vision/docs/" - }, - "public-datasets": { - "slug": "public-datasets", - "name": "Public Datasets", - "four_words": "Hosted data in BigQuery", - "doc_url": "https://cloud.google.com/bigquery/public-data" - }, - "cloud-tasks": { - "slug": "cloud-tasks", - "name": "Cloud Tasks", - "four_words": "Asynchronous task execution", - "doc_url": "https://cloud.google.com/tasks/docs/" - }, - "dataprep-by-trifacta": { - "slug": "dataprep-by-trifacta", - "name": "Dataprep by Trifacta", - "four_words": "Visual data wrangling", - "doc_url": "https://docs.trifacta.com/display/dp/" - }, - "appsheet": { - "slug": "appsheet", - "name": "AppSheet", - "four_words": "No-code App creation", - "doc_url": "https://cloud.google.com/appsheet" - }, - "cloud-deployment-manager": { - "slug": "cloud-deployment-manager", - "name": "Cloud Deployment Manager", - "four_words": "Templated infrastructure deployment", - "doc_url": "https://cloud.google.com/deployment-manager/docs/" - }, - "roads-api": { - "slug": "roads-api", - "name": "Roads API", - "four_words": "Convert coordinates to roads", - "doc_url": "https://developers.google.com/maps/documentation/roads/overview" - }, - "anthos-config-management": { - "slug": "anthos-config-management", - "name": "Anthos Config Management", - "four_words": "Policy and security automation", - "doc_url": "https://cloud.google.com/anthos-config-management/docs" - }, - "bigquery-data-transfer-service": { - "slug": "bigquery-data-transfer-service", - "name": "BigQuery Data Transfer Service", - "four_words": "Bulk import analytics data", - "doc_url": "https://cloud.google.com/bigquery-transfer/docs" - }, - "sole-tenant-nodes": { - "slug": "sole-tenant-nodes", - "name": "Sole-tenant Nodes", - "four_words": "Dedicated physical servers", - "doc_url": "https://cloud.google.com/compute/docs/nodes/sole-tenant-nodes" - }, - "cloud-code-for-intellij": { - "slug": "cloud-code-for-intellij", - "name": "Cloud Code for IntelliJ", - "four_words": "IntelliJ Google Cloud tools", - "doc_url": "https://cloud.google.com/code/docs" - }, - "packet-mirroring": { - "slug": "packet-mirroring", - "name": "Packet Mirroring", - "four_words": "Monitor / analyze instance traffic", - "doc_url": "https://cloud.google.com/vpc/docs/packet-mirroring" - }, - "cloud-armor": { - "slug": "cloud-armor", - "name": "Cloud Armor", - "four_words": "DDoS protection and WAF", - "doc_url": "https://cloud.google.com/armor/docs/" - }, - "cloud-deploy": { - "slug": "cloud-deploy", - "name": "Cloud Deploy", - "four_words": "Continuous Delivery for GKE", - "doc_url": "https://cloud.google.com/deploy/docs" - }, - "cloud-bigtable": { - "slug": "cloud-bigtable", - "name": "Cloud Bigtable", - "four_words": "Petabyte-scale, low-latency, non-relational", - "doc_url": "https://cloud.google.com/bigtable/docs/" - }, - "anthos-service-mesh": { - "slug": "anthos-service-mesh", - "name": "Anthos Service Mesh", - "four_words": "Managed service mesh (Istio)", - "doc_url": "https://cloud.google.com/service-mesh/docs" - }, - "cloud-trace": { - "slug": "cloud-trace", - "name": "Cloud Trace", - "four_words": "App latency insights", - "doc_url": "https://cloud.google.com/trace/docs/" - }, - "healthcare-natural-language-ai": { - "slug": "healthcare-natural-language-ai", - "name": "Healthcare Natural Language AI", - "four_words": "Real-time insights from media-text", - "doc_url": "https://cloud.google.com/healthcare-api/docs/how-tos/nlp" - }, - "migrate-from-amazon-redshift": { - "slug": "migrate-from-amazon-redshift", - "name": "Migrate from Amazon Redshift", - "four_words": "Migrate from Redshift to BigQuery", - "doc_url": "https://cloud.google.com/bigquery-transfer/docs/redshift-migration" - }, - "maps-sdk-for-android": { - "slug": "maps-sdk-for-android", - "name": "Maps SDK for Android", - "four_words": "Maps for Android apps", - "doc_url": "https://developers.google.com/maps/documentation/android-sdk/overview" - }, - "cloud-shell": { - "slug": "cloud-shell", - "name": "Cloud Shell", - "four_words": "Browser-based terminal / CLI", - "doc_url": "https://cloud.google.com/shell/docs/" - }, - "pubsub": { - "slug": "pubsub", - "name": "Pub/Sub", - "four_words": "Global real-time messaging", - "doc_url": "https://cloud.google.com/pubsub/docs/" - }, - "network-connectivity-center": { - "slug": "network-connectivity-center", - "name": "Network Connectivity Center", - "four_words": "Connect VPC & On-prem", - "doc_url": "https://cloud.google.com/network-connectivity/docs/network-connectivity-center/concepts/overview" - }, - "document-ai": { - "slug": "document-ai", - "name": "Document AI", - "four_words": "Analyze, classify, search documents", - "doc_url": "https://cloud.google.com/document-ai/docs" - }, - "dataplex": { - "slug": "dataplex", - "name": "Dataplex", - "four_words": "Centrally manage / monitor / govern data", - "doc_url": "https://cloud.google.com/dataplex" - }, - "firebase-test-lab": { - "slug": "firebase-test-lab", - "name": "Firebase Test Lab", - "four_words": "Mobile testing device farm", - "doc_url": "https://firebase.google.com/docs/test-lab/" - }, - "geocoding-api": { - "slug": "geocoding-api", - "name": "Geocoding API", - "four_words": "Convert address to/from coordinates", - "doc_url": "https://developers.google.com/maps/documentation/geocoding/overview" - }, - "cloud-functions": { - "slug": "cloud-functions", - "name": "Cloud Functions", - "four_words": "Event-driven serverless functions", - "doc_url": "https://cloud.google.com/functions/docs/" - }, - "security-key-enforcement": { - "slug": "security-key-enforcement", - "name": "Security Key Enforcement", - "four_words": "Two-step key verification", - "doc_url": "https://cloud.google.com/titan-security-key" - }, - "identity-platform": { - "slug": "identity-platform", - "name": "Identity Platform", - "four_words": "Drop-in Authentication, Access - Management", - "doc_url": "https://cloud.google.com/identity-platform/docs" - }, - "cloud-composer": { - "slug": "cloud-composer", - "name": "Cloud Composer", - "four_words": "Managed workflow orchestration service", - "doc_url": "https://cloud.google.com/composer/docs/" - }, - "cloud-identity": { - "slug": "cloud-identity", - "name": "Cloud Identity", - "four_words": "Manage users, devices & apps", - "doc_url": "https://cloud.google.com/identity/solutions/overview/" - }, - "assured-workloads": { - "slug": "assured-workloads", - "name": "Assured Workloads", - "four_words": "Workload compliance controls", - "doc_url": "https://cloud.google.com/assured-workloads/docs" - }, - "service-directory": { - "slug": "service-directory", - "name": "Service Directory", - "four_words": "Centrally publish / discover / connect services", - "doc_url": "https://cloud.google.com/service-directory/docs" - }, - "cloud-tools-for-eclipse": { - "slug": "cloud-tools-for-eclipse", - "name": "Cloud Tools for Eclipse", - "four_words": "Eclipse GCP tools", - "doc_url": "https://cloud.google.com/eclipse/docs/" - }, - "cloud-spanner": { - "slug": "cloud-spanner", - "name": "Cloud Spanner", - "four_words": "Horizontally scaleable relational database", - "doc_url": "https://cloud.google.com/spanner/docs/" - }, - "recommendations-ai": { - "slug": "recommendations-ai", - "name": "Recommendations AI", - "four_words": "Create custom recommendations", - "doc_url": "https://cloud.google.com/recommendations-ai/docs/" - }, - "cloud-apis": { - "slug": "cloud-apis", - "name": "Cloud APIs", - "four_words": "APIs for cloud services", - "doc_url": "https://cloud.google.com/apis/docs/overview" - }, - "apigee-healthcare-apix": { - "slug": "apigee-healthcare-apix", - "name": "Apigee Healthcare APIx", - "four_words": "Healthcare system GCP interoperability", - "doc_url": "https://cloud.google.com/solutions/apigee-health-apix#section-4" - }, - "email-markup": { - "slug": "email-markup", - "name": "Email Markup", - "four_words": "Interactive email using schema.org", - "doc_url": "https://developers.google.com/gmail/markup/overview" - }, - "eventarc": { - "slug": "eventarc", - "name": "Eventarc", - "four_words": "Event-driven Cloud Run services", - "doc_url": "https://cloud.google.com/eventarc/docs/" - }, - "vpc-service-controls": { - "slug": "vpc-service-controls", - "name": "VPC Service Controls", - "four_words": "VPC data constraints", - "doc_url": "https://cloud.google.com/vpc-service-controls/docs/" - }, - "vertex-ai-training": { - "slug": "vertex-ai-training", - "name": "Vertex AI Training", - "four_words": "Distributed AI training", - "doc_url": "https://cloud.google.com/ai-platform/training/docs/overview" - }, - "automl": { - "slug": "automl", - "name": "AutoML", - "four_words": "Custom low-code models", - "doc_url": "https://cloud.google.com/vertex-ai/docs/start/automl-users" - }, - "connected-sheets": { - "slug": "connected-sheets", - "name": "Connected Sheets", - "four_words": "Spreadsheet interface for (big) data", - "doc_url": "https://cloud.google.com/bigquery/docs/connected-sheets" - }, - "preemptible-vms": { - "slug": "preemptible-vms", - "name": "Preemptible VMs", - "four_words": "Short-lived compute instances", - "doc_url": "https://cloud.google.com/compute/docs/instances/preemptible" - }, - "apigee-sense": { - "slug": "apigee-sense", - "name": "Apigee Sense", - "four_words": "API protection from attacks", - "doc_url": "https://cloud.google.com/apigee/sense" - }, - "cloud-ids": { - "slug": "cloud-ids", - "name": "Cloud IDS", - "four_words": "Detects Network based threats", - "doc_url": "https://cloud.google.com/intrusion-detection-system/docs" - }, - "firebase-remote-config": { - "slug": "firebase-remote-config", - "name": "Firebase Remote Config", - "four_words": "Remotely configure installed apps", - "doc_url": "https://firebase.google.com/docs/remote-config/" - }, - "vertex-ai": { - "slug": "vertex-ai", - "name": "Vertex AI", - "four_words": "Managed platform for ML", - "doc_url": "https://cloud.google.com/vertex-ai/docs" - }, - "directions-api": { - "slug": "directions-api", - "name": "Directions API", - "four_words": "Get directions between locations", - "doc_url": "https://developers.google.com/maps/documentation/directions/overview" - }, - "apigee-api-management": { - "slug": "apigee-api-management", - "name": "Apigee API Management", - "four_words": "API management, development, security", - "doc_url": "https://cloud.google.com/apigee/docs" - }, - "gmail-api": { - "slug": "gmail-api", - "name": "Gmail API", - "four_words": "Enhance Gmail", - "doc_url": "https://developers.google.com/gmail/api" - }, - "cloud-firestore": { - "slug": "cloud-firestore", - "name": "Cloud Firestore", - "four_words": "Serverless NoSQL document DB", - "doc_url": "https://cloud.google.com/firestore/docs/" - }, - "deep-learning-vm-images": { - "slug": "deep-learning-vm-images", - "name": "Deep Learning VM Images ", - "four_words": "Preconfigured VMs for deep learning", - "doc_url": "https://cloud.google.com/deep-learning-vm/docs/" - }, - "maps-urls": { - "slug": "maps-urls", - "name": "Maps URLs", - "four_words": "URL scheme for maps", - "doc_url": "https://developers.google.com/maps/documentation/urls/get-started" - }, - "binary-authorization": { - "slug": "binary-authorization", - "name": "Binary Authorization", - "four_words": "Kubernetes deploy-time security", - "doc_url": "https://cloud.google.com/binary-authorization/docs/" - }, - "vertex-ai-predictions": { - "slug": "vertex-ai-predictions", - "name": "Vertex AI Predictions", - "four_words": "Autoscaled model serving", - "doc_url": "https://cloud.google.com/vertex-ai/docs/" - }, - "google-cloud-marketplace": { - "slug": "google-cloud-marketplace", - "name": "Google Cloud Marketplace", - "four_words": "Partner & open source marketplace", - "doc_url": "https://cloud.google.com/marketplace/docs" - }, - "contact-center-ai": { - "slug": "contact-center-ai", - "name": "Contact Center AI", - "four_words": "AI in your contact center", - "doc_url": "https://cloud.google.com/solutions/contact-center" - }, - "dataproc": { - "slug": "dataproc", - "name": "Dataproc", - "four_words": "Managed Spark and Hadoop", - "doc_url": "https://cloud.google.com/dataproc/docs/" - }, - "maps-sdk-for-ios": { - "slug": "maps-sdk-for-ios", - "name": "Maps SDK for iOS", - "four_words": "Maps for iOS apps", - "doc_url": "https://developers.google.com/maps/documentation/ios-sdk/overview" - }, - "cloud-sql": { - "slug": "cloud-sql", - "name": "Cloud SQL", - "four_words": "Managed MySQL, PostgreSQL, SQL Server", - "doc_url": "https://cloud.google.com/sql/docs/" - }, - "maps-embed-api": { - "slug": "maps-embed-api", - "name": "Maps Embed API", - "four_words": "Display iframe embedded maps", - "doc_url": "https://developers.google.com/maps/documentation/embed/get-started" - }, - "distance-matrix-api": { - "slug": "distance-matrix-api", - "name": "Distance Matrix API", - "four_words": "Multi-origin / destination travel times", - "doc_url": "https://developers.google.com/maps/documentation/distance-matrix/start" - }, - "cloud-data-loss-prevention": { - "slug": "cloud-data-loss-prevention", - "name": "Cloud Data Loss Prevention", - "four_words": "Classify and redact sensitive data", - "doc_url": "https://cloud.google.com/dlp/docs/" - }, - "firebase-extensions": { - "slug": "firebase-extensions", - "name": "Firebase Extensions", - "four_words": "Pre-packaged development solutions", - "doc_url": "https://firebase.google.com/docs/extensions" - }, - "cloud-data-transfer": { - "slug": "cloud-data-transfer", - "name": "Cloud Data Transfer", - "four_words": "Data migration tools/CLI", - "doc_url": "https://cloud.google.com/storage-transfer/docs" - }, - "google-cloud-marketplace-for-anthos": { - "slug": "google-cloud-marketplace-for-anthos", - "name": "Google Cloud Marketplace for Anthos", - "four_words": "Pre-configured containerized apps", - "doc_url": "https://cloud.google.com/marketplace/docs/kubernetes-apps" - }, - "api-monetization": { - "slug": "api-monetization", - "name": "API Monetization", - "four_words": "Monetize APIs", - "doc_url": "https://cloud.google.com/apigee/docs" - }, - "cloud-run": { - "slug": "cloud-run", - "name": "Cloud Run", - "four_words": "Serverless for containerized applications", - "doc_url": "https://cloud.google.com/run/docs" - }, - "transcoder-api": { - "slug": "transcoder-api", - "name": "Transcoder API", - "four_words": "Optimized files for delivery", - "doc_url": "https://cloud.google.com/transcoder/docs" - }, - "firebase-performance-monitoring": { - "slug": "firebase-performance-monitoring", - "name": "Firebase Performance Monitoring", - "four_words": "App/web performance monitoring", - "doc_url": "https://firebase.google.com/docs/perf-mon" - }, - "firebase-ab-testing": { - "slug": "firebase-ab-testing", - "name": "Firebase A/B Testing", - "four_words": "Create A/B test experiments", - "doc_url": "https://firebase.google.com/docs/ab-testing" - }, - "cloud-code-for-vs-code": { - "slug": "cloud-code-for-vs-code", - "name": "Cloud Code for VS Code", - "four_words": "VS Code Google Cloud tools", - "doc_url": "https://cloud.google.com/code/docs/vscode/" - }, - "firebase-cloud-messaging": { - "slug": "firebase-cloud-messaging", - "name": "Firebase Cloud Messaging", - "four_words": "Send device notifications", - "doc_url": "https://firebase.google.com/docs/cloud-messaging" - }, - "vertex-ai-pipelines": { - "slug": "vertex-ai-pipelines", - "name": "Vertex AI Pipelines", - "four_words": "Hosted ML workflows", - "doc_url": "https://cloud.google.com/ai-platform/pipelines/docs" - }, - "workflows": { - "slug": "workflows", - "name": "Workflows", - "four_words": "HTTP services orchestration", - "doc_url": "https://cloud.google.com/workflows/docs/" - }, - "anthos-clusters": { - "slug": "anthos-clusters", - "name": "Anthos Clusters", - "four_words": "Hybrid / on-prem GKE", - "doc_url": "https://cloud.google.com/anthos/clusters/docs/on-prem/1.9" - }, - "crashlytics": { - "slug": "crashlytics", - "name": "Crashlytics", - "four_words": "Crash reporting and analytics", - "doc_url": "https://firebase.google.com/docs/crashlytics" - }, - "calendar-api": { - "slug": "calendar-api", - "name": "Calendar API", - "four_words": "Create and manage calendars", - "doc_url": "https://developers.google.com/apps-script/add-ons/calendar" - }, - "visual-inspection-ai": { - "slug": "visual-inspection-ai", - "name": "Visual Inspection AI", - "four_words": "Train / deploy models to detect defects", - "doc_url": "https://cloud.google.com/solutions/visual-inspection-ai" - }, - "cloud-mobile-app": { - "slug": "cloud-mobile-app", - "name": "Cloud Mobile App", - "four_words": "iOS / Android Google Cloud manager app", - "doc_url": "https://cloud.google.com/console-app/" - }, - "cloud-translation": { - "slug": "cloud-translation", - "name": "Cloud Translation", - "four_words": "Language detection and translation", - "doc_url": "https://cloud.google.com/translate/docs/" - }, - "firebase-dynamic-links": { - "slug": "firebase-dynamic-links", - "name": "Firebase Dynamic Links", - "four_words": "Link to app content", - "doc_url": "https://firebase.google.com/docs/dynamic-links" - }, - "migrate-for-anthos-and-gke": { - "slug": "migrate-for-anthos-and-gke", - "name": "Migrate for Anthos and GKE", - "four_words": "Migrate VMs to GKE", - "doc_url": "https://cloud.google.com/migrate/anthos/docs/getting-started" - }, - "sheets-api": { - "slug": "sheets-api", - "name": "Sheets API", - "four_words": "Read and write spreadsheets", - "doc_url": "https://developers.google.com/sheets/api/reference/rest" - }, - "alloydb": { - "slug": "alloydb", - "name": "AlloyDB", - "four_words": "Scalable & performant PostgreSQL - compatible DB", - "doc_url": "https://cloud.google.com/alloydb/docs" - }, - "app-engine": { - "slug": "app-engine", - "name": "App Engine", - "four_words": "Managed app platform", - "doc_url": "https://cloud.google.com/appengine/docs/" - }, - "time-zone-api": { - "slug": "time-zone-api", - "name": "Time Zone API", - "four_words": "Convert coordinates to timezone", - "doc_url": "https://developers.google.com/maps/documentation/timezone/get-started" - }, - "video-intelligence-api": { - "slug": "video-intelligence-api", - "name": "Video Intelligence API", - "four_words": "Scene-level video annotation", - "doc_url": "https://cloud.google.com/video-intelligence/docs/" - }, - "private-catalog": { - "slug": "private-catalog", - "name": "Private Catalog", - "four_words": "Internal Solutions Catalog", - "doc_url": "https://cloud.google.com/private-catalog/docs/" - }, - "cloud-load-balancing": { - "slug": "cloud-load-balancing", - "name": "Cloud Load Balancing", - "four_words": "Multi-region load distribution / balancing", - "doc_url": "https://cloud.google.com/load-balancing/docs" - }, - "bigquery-dts": { - "slug": "bigquery-dts", - "name": "BigQuery DTS", - "four_words": "Automated data ingestion service", - "doc_url": "https://cloud.google.com/bigquery-transfer/docs" - }, - "vertex-ai-data-labeling": { - "slug": "vertex-ai-data-labeling", - "name": "Vertex AI Data Labeling", - "four_words": "Data labeling by humans", - "doc_url": "https://cloud.google.com/ai-platform/data-labeling/docs" - }, - "chronicle": { - "slug": "chronicle", - "name": "Chronicle", - "four_words": "Find threats from security telemetry", - "doc_url": "https://cloud.google.com/chronicle/docs" - }, - "api-analytics": { - "slug": "api-analytics", - "name": "API Analytics", - "four_words": "API metrics", - "doc_url": "https://cloud.google.com/apigee/docs" - }, - "shielded-vms": { - "slug": "shielded-vms", - "name": "Shielded VMs", - "four_words": "Hardened VMs", - "doc_url": "https://cloud.google.com/security/shielded-cloud/shielded-vm/" - }, - "places-sdk-for-ios": { - "slug": "places-sdk-for-ios", - "name": "Places SDK for iOS", - "four_words": "Places feature for iOS", - "doc_url": "https://developers.google.com/maps/documentation/places/ios-sdk/start" - }, - "cloud-sdk": { - "slug": "cloud-sdk", - "name": "Cloud SDK", - "four_words": "Google Cloud CLI", - "doc_url": "https://cloud.google.com/sdk/docs/" - }, - "virustotal": { - "slug": "virustotal", - "name": "VirusTotal", - "four_words": "Research / hunt for malware", - "doc_url": "https://cloud.google.com/chronicle/docs/investigation/view-virustotal-information" - }, - "anthos": { - "slug": "anthos", - "name": "Anthos", - "four_words": "Enterprise hybrid / multi-cloud platform", - "doc_url": "https://cloud.google.com/anthos/docs/" - }, - "dedicated-interconnect": { - "slug": "dedicated-interconnect", - "name": "Dedicated Interconnect", - "four_words": "Dedicated private network connection", - "doc_url": "https://cloud.google.com/network-connectivity/docs/interconnect/concepts/dedicated-overview" - }, - "cloud-run-for-anthos": { - "slug": "cloud-run-for-anthos", - "name": "Cloud Run for Anthos", - "four_words": "Serverless development for Anthos", - "doc_url": "https://cloud.google.com/anthos/run/docs/quickstarts/prebuilt-deploy-gke" - }, - "cloud-nat": { - "slug": "cloud-nat", - "name": "Cloud NAT", - "four_words": "Network address translation service", - "doc_url": "https://cloud.google.com/nat/docs" - }, - "gke": { - "slug": "gke", - "name": "Kubernetes Engine", - "four_words": "Managed Kubernetes / containers", - "doc_url": "https://cloud.google.com/kubernetes-engine/docs/" - }, - "media-cdn": { - "slug": "media-cdn", - "name": "Media CDN", - "four_words": "CDN for streaming & videos", - "doc_url": "https://cloud.google.com/media-cdn/docs/overview" - }, - "cloud-storage-for-firebase": { - "slug": "cloud-storage-for-firebase", - "name": "Cloud Storage for Firebase", - "four_words": "Object storage and serving", - "doc_url": "https://firebase.google.com/docs/storage" - }, - "admin-sdk": { - "slug": "admin-sdk", - "name": "Admin SDK", - "four_words": "Manage Google Workspace resources", - "doc_url": "https://developers.google.com/apps-script/advanced/admin-sdk-directory" - }, - "google-analytics-for-firebase": { - "slug": "google-analytics-for-firebase", - "name": "Google Analytics for Firebase", - "four_words": "Mobile app analytics", - "doc_url": "https://firebase.google.com/docs/analytics" - }, - "vm-manager": { - "slug": "vm-manager", - "name": "VM Manager", - "four_words": "Manage OS VM Fleets", - "doc_url": "https://cloud.google.com/compute/docs/vm-manager" - }, - "error-reporting": { - "slug": "error-reporting", - "name": "Error Reporting", - "four_words": "App error reporting", - "doc_url": "https://cloud.google.com/error-reporting/docs/" - }, - "beyondcorp-enterprise": { - "slug": "beyondcorp-enterprise", - "name": "BeyondCorp Enterprise", - "four_words": "Zero trust secure access", - "doc_url": "https://cloud.google.com/beyondcorp-enterprise/docs" - }, - "dataflow": { - "slug": "dataflow", - "name": "Dataflow", - "four_words": "Stream / batch data processing", - "doc_url": "https://cloud.google.com/dataflow/docs/" - }, - "risk-manager": { - "slug": "risk-manager", - "name": "Risk Manager", - "four_words": "Evaluate organization's security posture", - "doc_url": "https://cloud.google.com/risk-manager/docs" - }, - "kf": { - "slug": "kf", - "name": "KF", - "four_words": "Cloud Foundry to Kubernetes", - "doc_url": "https://cloud.google.com/migrate/kf/docs/2.6" - }, - "vision-product-search": { - "slug": "vision-product-search", - "name": "Vision Product Search", - "four_words": "Visual search for products", - "doc_url": "https://cloud.google.com/vision/product-search/docs/" - }, - "carrier-peering": { - "slug": "carrier-peering", - "name": "Carrier Peering", - "four_words": "Peer through a carrier", - "doc_url": "https://cloud.google.com/network-connectivity/docs/carrier-peering" - }, - "private-service-connect": { - "slug": "private-service-connect", - "name": "Private Service Connect", - "four_words": "Privately connect services across VPCs", - "doc_url": "https://cloud.google.com/vpc/docs/private-service-connect" - }, - "cloud-audit-logs": { - "slug": "cloud-audit-logs", - "name": "Cloud Audit Logs", - "four_words": "Audit trails", - "doc_url": "https://cloud.google.com/logging/docs/audit/" - }, - "vertex-explainable-ai": { - "slug": "vertex-explainable-ai", - "name": "Vertex Explainable AI", - "four_words": "Understand ML model predictions", - "doc_url": "https://cloud.google.com/vertex-ai/docs/explainable-ai" - }, - "places-api": { - "slug": "places-api", - "name": "Places API", - "four_words": "Rest-based Places features", - "doc_url": "https://developers.google.com/maps/documentation/places/web-service/overview" - }, - "vault-api": { - "slug": "vault-api", - "name": "Vault API", - "four_words": "Manage your organization's eDiscovery", - "doc_url": "https://developers.google.com/vault/reference/rest" - }, - "managed-service-for-microsoft-active-directory": { - "slug": "managed-service-for-microsoft-active-directory", - "name": "Managed Service for Microsoft Active Directory", - "four_words": "Managed Microsoft Active Directory", - "doc_url": "https://cloud.google.com/managed-microsoft-ad/docs/" - }, - "bigquery-gis": { - "slug": "bigquery-gis", - "name": "BigQuery GIS", - "four_words": "BigQuery geospatial functions / support", - "doc_url": "https://cloud.google.com/bigquery/docs/geospatial-intro" - }, - "cloud-iam": { - "slug": "cloud-iam", - "name": "Cloud IAM", - "four_words": "Resource access control", - "doc_url": "https://cloud.google.com/iam/docs/" - }, - "cloud-cdn": { - "slug": "cloud-cdn", - "name": "Cloud CDN", - "four_words": "Content delivery network", - "doc_url": "https://cloud.google.com/cdn/docs/" - }, - "app-engine-plugins": { - "slug": "app-engine-plugins", - "name": "App Engine Plugins", - "four_words": "Gradle / Maven App Engine plugin", - "doc_url": "https://cloud.google.com/appengine/docs/standard/java/gradle-reference" - }, - "vertex-ai-vizier": { - "slug": "vertex-ai-vizier", - "name": "Vertex AI Vizier", - "four_words": "Automated hyperparameter tuning", - "doc_url": "https://cloud.google.com/vertex-ai/docs/vizier" - }, - "firebase-realtime-database": { - "slug": "firebase-realtime-database", - "name": "Firebase Realtime Database", - "four_words": "Real-time data synchronization", - "doc_url": "https://firebase.google.com/docs/database" - }, - "certificate-authority-service": { - "slug": "certificate-authority-service", - "name": "Certificate Authority Service", - "four_words": "Managed private CAs", - "doc_url": "https://cloud.google.com/certificate-authority-service/docs" - }, - "bigquery-bi-engine": { - "slug": "bigquery-bi-engine", - "name": "BigQuery BI Engine", - "four_words": "In-memory analytics engine", - "doc_url": "https://cloud.google.com/bi-engine/docs/" - }, - "virtual-private-cloud": { - "slug": "virtual-private-cloud", - "name": "Virtual Private Cloud", - "four_words": "Software defined networking", - "doc_url": "https://cloud.google.com/vpc/docs/" - }, - "firebase-in-app-messaging": { - "slug": "firebase-in-app-messaging", - "name": "Firebase In-App Messaging", - "four_words": "Send in-app contextual messages", - "doc_url": "https://firebase.google.com/docs/in-app-messaging" - }, - "drive-activity-api": { - "slug": "drive-activity-api", - "name": "Drive Activity API", - "four_words": "Retrieve Google Drive activity", - "doc_url": "https://developers.google.com/drive/activity/" - }, - "google-chat-api": { - "slug": "google-chat-api", - "name": "Google Chat API", - "four_words": "Conversational bots in chat", - "doc_url": "https://developers.google.com/chat" - }, - "cloud-code": { - "slug": "cloud-code", - "name": "Cloud Code", - "four_words": "Google Cloud IDE extensions", - "doc_url": "https://cloud.google.com/code/docs/" - }, - "speech-to-text": { - "slug": "speech-to-text", - "name": "Speech-To-Text", - "four_words": "Convert audio to text", - "doc_url": "https://cloud.google.com/speech-to-text/docs" - }, - "firebase-hosting": { - "slug": "firebase-hosting", - "name": "Firebase Hosting", - "four_words": "Web hosting with CDN/SSL", - "doc_url": "https://firebase.google.com/docs/hosting" - }, - "vertex-ai-feature-store": { - "slug": "vertex-ai-feature-store", - "name": "Vertex AI Feature Store", - "four_words": "Managed ML feature repository", - "doc_url": "https://cloud.google.com/vertex-ai/docs/featurestore" - }, - "places-sdk-for-android": { - "slug": "places-sdk-for-android", - "name": "Places SDK for Android", - "four_words": "Places features for Android", - "doc_url": "https://developers.google.com/maps/documentation/places/android-sdk/start" - }, - "firebase-predictions": { - "slug": "firebase-predictions", - "name": "Firebase Predictions", - "four_words": "Predict user targeting", - "doc_url": "https://firebase.google.com/docs/predictions" - }, - "resource-manager": { - "slug": "resource-manager", - "name": "Resource Manager", - "four_words": "Cloud project metadata management", - "doc_url": "https://cloud.google.com/resource-manager/docs" - }, - "cloud-filestore": { - "slug": "cloud-filestore", - "name": "Cloud Filestore", - "four_words": "Managed NFS server", - "doc_url": "https://cloud.google.com/filestore/docs/" - }, - "developer-portal": { - "slug": "developer-portal", - "name": "Developer Portal", - "four_words": "API management portal", - "doc_url": "https://cloud.google.com/apigee/docs" - }, - "cloud-console": { - "slug": "cloud-console", - "name": "Cloud Console", - "four_words": "Web-based management console", - "doc_url": "https://cloud.google.com/cloud-console/" - }, - "migrate-for-anthos": { - "slug": "migrate-for-anthos", - "name": "Migrate for Anthos", - "four_words": "Migrate VMs to Kubernetes Engine", - "doc_url": "https://cloud.google.com/migrate/anthos/docs/getting-started" - }, - "datastream": { - "slug": "datastream", - "name": "Datastream", - "four_words": "Change data capture / replication service", - "doc_url": "https://cloud.google.com/datastream/docs" - }, - "cloud-ekm": { - "slug": "cloud-ekm", - "name": "Cloud EKM", - "four_words": "External keys you control / manage", - "doc_url": "https://cloud.google.com/kms/docs/ekm/" - }, - "google-cloud-game-servers": { - "slug": "google-cloud-game-servers", - "name": "Google Cloud Game Servers", - "four_words": "Orchestrate Agones clusters", - "doc_url": "https://cloud.google.com/game-servers/docs" - }, - "apigee-hybrid": { - "slug": "apigee-hybrid", - "name": "Apigee Hybrid", - "four_words": "Manage hybrid / multi-cloud API environments", - "doc_url": "https://cloud.google.com/apigee/docs/hybrid/v1.6/what-is-hybrid" - }, - "street-view-static-api": { - "slug": "street-view-static-api", - "name": "Street View Static API", - "four_words": "Static street view images", - "doc_url": "https://developers.google.com/maps/documentation/streetview/overview" - }, - "context-aware-access": { - "slug": "context-aware-access", - "name": "Context-aware Access", - "four_words": "End-user attribute-based access control", - "doc_url": "https://cloud.google.com/iap/docs/cloud-iap-context-aware-access-howto/" - }, - "vmware-engine": { - "slug": "vmware-engine", - "name": "VMware Engine", - "four_words": "VMware as a service", - "doc_url": "https://cloud.google.com/vmware-engine/docs" - }, - "maps-static-api": { - "slug": "maps-static-api", - "name": "Maps Static API", - "four_words": "Display static map images", - "doc_url": "https://developers.google.com/maps/documentation/maps-static/start" - }, - "cloud-foundation-toolkit": { - "slug": "cloud-foundation-toolkit", - "name": "Cloud Foundation Toolkit", - "four_words": "Infrastructure as Code templates", - "doc_url": "https://github.com/GoogleCloudPlatform/cloud-foundation-toolkit/blob/master/docs/terraform.md" - }, - "drive-picker": { - "slug": "drive-picker", - "name": "Drive Picker", - "four_words": "Drive file selection widget", - "doc_url": "https://developers.google.com/picker/docs/reference" - }, - "drive-api": { - "slug": "drive-api", - "name": "Drive API", - "four_words": "Read and write files", - "doc_url": "https://developers.google.com/apps-script/reference/drive" - }, - "vertex-ai-model-monitoring": { - "slug": "vertex-ai-model-monitoring", - "name": "Vertex AI Model Monitoring", - "four_words": "Monitor models for skew/drift", - "doc_url": "https://cloud.google.com/vertex-ai/docs/model-monitoring" - }, - "database-migration-service": { - "slug": "database-migration-service", - "name": "Database Migration Service", - "four_words": "Migrate to Cloud SQL", - "doc_url": "https://cloud.google.com/database-migration/docs" - }, - "secret-manager": { - "slug": "secret-manager", - "name": "Secret Manager", - "four_words": "Store and manage secrets", - "doc_url": "https://cloud.google.com/secret-manager/docs/" - }, - "google-workspace-marketplace": { - "slug": "google-workspace-marketplace", - "name": "Google Workspace Marketplace", - "four_words": "Storefront for integrated applications", - "doc_url": "https://developers.google.com/workspace/marketplace/reference/rest" - }, - "vertex-ml-metadata": { - "slug": "vertex-ml-metadata", - "name": "Vertex ML Metadata", - "four_words": "Artifact, lineage, and execution tracking", - "doc_url": "https://cloud.google.com/vertex-ai/docs/ml-metadata" - }, - "cloud-endpoints": { - "slug": "cloud-endpoints", - "name": "Cloud Endpoints", - "four_words": "Cloud API gateway", - "doc_url": "https://cloud.google.com/endpoints/docs" - }, - "firebase-app-distribution": { - "slug": "firebase-app-distribution", - "name": "Firebase App Distribution", - "four_words": "Trusted tester early access", - "doc_url": "https://firebase.google.com/docs/app-distribution" - }, - "network-telemetry": { - "slug": "network-telemetry", - "name": "Network Telemetry", - "four_words": "Network telemetry service", - "doc_url": "https://cloud.google.com/vpc/docs/using-flow-logs/" - }, - "cloud-identity-aware-proxy": { - "slug": "cloud-identity-aware-proxy", - "name": "Cloud Identity-Aware Proxy", - "four_words": "Identity-based app access", - "doc_url": "https://cloud.google.com/iap/docs/" - }, - "transfer-appliance": { - "slug": "transfer-appliance", - "name": "Transfer Appliance", - "four_words": "Rentable data transport box", - "doc_url": "https://cloud.google.com/transfer-appliance/docs/4.0" - }, - "maps-javascript-api": { - "slug": "maps-javascript-api", - "name": "Maps JavaScript API", - "four_words": "Dynamic web maps", - "doc_url": "https://developers.google.com/maps/documentation/javascript/overview" - }, - "cloud-scheduler": { - "slug": "cloud-scheduler", - "name": "Cloud Scheduler", - "four_words": "Managed cron job service", - "doc_url": "https://cloud.google.com/scheduler/docs/" - }, - "storage-transfer-service": { - "slug": "storage-transfer-service", - "name": "Storage Transfer Service", - "four_words": "Online / on-premises data transfer", - "doc_url": "https://cloud.google.com/storage-transfer/docs" - }, - "network-intelligence-center": { - "slug": "network-intelligence-center", - "name": "Network Intelligence Center", - "four_words": "Network monitoring and topology", - "doc_url": "https://cloud.google.com/network-intelligence-center/docs/" - }, - "persistent-disk": { - "slug": "persistent-disk", - "name": "Persistent Disk", - "four_words": "Block storage for VMs", - "doc_url": "https://cloud.google.com/compute/docs/disks/" - }, - "google-workspace-add-ons": { - "slug": "google-workspace-add-ons", - "name": "Google Workspace Add-ons", - "four_words": "Extend Google Workspace apps", - "doc_url": "https://developers.google.com/apps-script/add-ons/overview" - }, - "cloud-storage": { - "slug": "cloud-storage", - "name": "Cloud Storage", - "four_words": "Multi-class multi-region object storage", - "doc_url": "https://cloud.google.com/storage/docs/" - }, - "cloud-life-sciences": { - "slug": "cloud-life-sciences", - "name": "Cloud Life Sciences", - "four_words": "Manage, process, transform biomedical-data", - "doc_url": "https://cloud.google.com/life-sciences/docs" - }, - "street-view-service": { - "slug": "street-view-service", - "name": "Street View Service", - "four_words": "Street view for JavaScript", - "doc_url": "https://developers.google.com/maps/documentation/javascript/streetview/" - }, - "bigquery-ml": { - "slug": "bigquery-ml", - "name": "BigQuery ML", - "four_words": "BigQuery model training / serving", - "doc_url": "https://cloud.google.com/bigquery-ml/docs/" - }, - "partner-interconnect": { - "slug": "partner-interconnect", - "name": "Partner Interconnect", - "four_words": "Connect on-prem network to VPC", - "doc_url": "https://cloud.google.com/network-connectivity/docs/interconnect/concepts/partner-overview" - }, - "cloud-hsm": { - "slug": "cloud-hsm", - "name": "Cloud HSM", - "four_words": "Hardware security module service", - "doc_url": "https://cloud.google.com/kms/docs/hsm/" - }, - "migrate-for-compute-engine": { - "slug": "migrate-for-compute-engine", - "name": "Migrate for Compute Engine", - "four_words": "Compute Engine migration tools", - "doc_url": "https://cloud.google.com/migrate/compute-engine/docs/5.0" - }, - "slides-api": { - "slug": "slides-api", - "name": "Slides API", - "four_words": "Create and edit presentations", - "doc_url": "https://developers.google.com/slides/api" - }, - "people-api": { - "slug": "people-api", - "name": "People API", - "four_words": "Manage user's Contacts", - "doc_url": "https://developers.google.com/people/api/rest" - }, - "artifact-registry": { - "slug": "artifact-registry", - "name": "Artifact Registry", - "four_words": "Universal package manager", - "doc_url": "https://cloud.google.com/artifact-registry/docs" - }, - "cloud-data-fusion": { - "slug": "cloud-data-fusion", - "name": "Cloud Data Fusion", - "four_words": "Graphically manage data pipelines", - "doc_url": "https://cloud.google.com/data-fusion/docs/" - }, - "cloud-sql-insights": { - "slug": "cloud-sql-insights", - "name": "Cloud SQL Insights", - "four_words": "SQL Inspector", - "doc_url": "https://cloud.google.com/sql/docs/postgres/using-query-insights" - }, - "cloud-billing": { - "slug": "cloud-billing", - "name": "Cloud Billing", - "four_words": "Billing and cost management tools", - "doc_url": "https://cloud.google.com/billing/docs/" - }, - "cloud-source-repositories": { - "slug": "cloud-source-repositories", - "name": "Cloud Source Repositories", - "four_words": "Hosted private git repos", - "doc_url": "https://cloud.google.com/source-repositories/docs/" - }, - "compute-engine": { - "slug": "compute-engine", - "name": "Compute Engine", - "four_words": "VMs, GPUs, TPUs, Disks", - "doc_url": "https://cloud.google.com/compute/docs/" - }, - "cloud-dns": { - "slug": "cloud-dns", - "name": "Cloud DNS", - "four_words": "Programmable DNS serving", - "doc_url": "https://cloud.google.com/dns/docs/" - }, - "cloud-monitoring": { - "slug": "cloud-monitoring", - "name": "Cloud Monitoring", - "four_words": "Infrastructure and application monitoring", - "doc_url": "https://cloud.google.com/monitoring/docs/" - }, - "cloud-asset-inventory": { - "slug": "cloud-asset-inventory", - "name": "Cloud Asset Inventory", - "four_words": "All assets, one place", - "doc_url": "https://cloud.google.com/asset-inventory/docs/overview" - }, - "cloud-kms": { - "slug": "cloud-kms", - "name": "Cloud KMS", - "four_words": "Hosted key management service", - "doc_url": "https://cloud.google.com/kms/docs/" - }, - "places-library-maps-js-api": { - "slug": "places-library-maps-js-api", - "name": "Places Library, Maps JS API", - "four_words": "Places features for web", - "doc_url": "https://developers.google.com/maps/documentation/javascript/places" - }, - "cloud-tpu": { - "slug": "cloud-tpu", - "name": "Cloud TPU", - "four_words": "Hardware acceleration for ML", - "doc_url": "https://cloud.google.com/tpu/docs/" - }, - "geolocation-api": { - "slug": "geolocation-api", - "name": "Geolocation API", - "four_words": "Derive location without GPS", - "doc_url": "https://developers.google.com/maps/documentation/geolocation/overview" - }, - "cloud-functions-for-firebase": { - "slug": "cloud-functions-for-firebase", - "name": "Cloud Functions for Firebase", - "four_words": "Event-driven serverless applications", - "doc_url": "https://firebase.google.com/docs/functions" - }, - "vertex-ai-edge-manager": { - "slug": "vertex-ai-edge-manager", - "name": "Vertex AI Edge Manager", - "four_words": "Deploy monitor edge inferences", - "doc_url": "https://cloud.google.com/vertex-ai" - }, - "cloud-tools-for-visual-studio": { - "slug": "cloud-tools-for-visual-studio", - "name": "Cloud Tools for Visual Studio", - "four_words": "Visual Studio Google Cloud tools", - "doc_url": "https://cloud.google.com/tools/visual-studio/docs" - }, - "cloud-vpn": { - "slug": "cloud-vpn", - "name": "Cloud VPN", - "four_words": "Virtual private network connection", - "doc_url": "https://cloud.google.com/network-connectivity/docs/vpn/concepts/overview" - }, - "cloud-billing-api": { - "slug": "cloud-billing-api", - "name": "Cloud Billing API", - "four_words": "Programmatically manage Google Cloud billing", - "doc_url": "https://cloud.google.com/billing/docs/" - }, - "direct-peering": { - "slug": "direct-peering", - "name": "Direct Peering", - "four_words": "Peer with Google Cloud", - "doc_url": "https://cloud.google.com/network-connectivity/docs/direct-peering" - }, - "amp-for-email": { - "slug": "amp-for-email", - "name": "AMP for Email", - "four_words": "Dynamic interactive email", - "doc_url": "https://developers.google.com/apps-script/add-ons/gmail" - }, - "cloud-search": { - "slug": "cloud-search", - "name": "Cloud Search", - "four_words": "Unified search for enterprise", - "doc_url": "https://developers.google.com/cloud-search/docs/guides/" - }, - "operations-suite": { - "slug": "operations-suite", - "name": "Operations suite", - "four_words": "Monitoring, logging, troubleshooting", - "doc_url": "https://cloud.google.com/stackdriver/docs" - }, - "confidential-computing": { - "slug": "confidential-computing", - "name": "Confidential Computing", - "four_words": "Encrypt data in-use", - "doc_url": "https://cloud.google.com/compute/confidential-vm/docs" - }, - "talent-solutions": { - "slug": "talent-solutions", - "name": "Talent Solutions", - "four_words": "Job search with ML", - "doc_url": "https://cloud.google.com/talent-solution/docs" - }, - "classroom-api": { - "slug": "classroom-api", - "name": "Classroom API", - "four_words": "Provision and manage classrooms", - "doc_url": "https://developers.google.com/classroom/guides/get-started" - }, - "event-threat-detection": { - "slug": "event-threat-detection", - "name": "Event Threat Detection", - "four_words": "Scans for suspicious activity", - "doc_url": "https://cloud.google.com/security-command-center/docs/how-to-use-event-threat-detection" - }, - "cloud-build": { - "slug": "cloud-build", - "name": "Cloud Build", - "four_words": "DevOps Automation Platform", - "doc_url": "https://cloud.google.com/build/docs" - }, - "vertex-ai-tensorboard": { - "slug": "vertex-ai-tensorboard", - "name": "Vertex AI Tensorboard", - "four_words": "Managed TensorBoard for ML-experiment Visualization", - "doc_url": "https://cloud.google.com/vertex-ai/docs/experiments" - }, - "text-to-speech": { - "slug": "text-to-speech", - "name": "Text-To-Speech", - "four_words": "Convert text to audio", - "doc_url": "https://cloud.google.com/text-to-speech/docs/" - }, - "bare-metal-solution": { - "slug": "bare-metal-solution", - "name": "Bare Metal Solution", - "four_words": "Hardware for specialized workloads", - "doc_url": "https://cloud.google.com/bare-metal/docs/bms-planning" - }, - "data-studio": { - "slug": "data-studio", - "name": "Data Studio", - "four_words": "Collaborative data exploration / dashboarding", - "doc_url": "https://cloud.google.com/bigquery/docs/visualize-data-studio" - }, - "local-ssd": { - "slug": "local-ssd", - "name": "Local SSD", - "four_words": "VM locally attached SSDs", - "doc_url": "https://cloud.google.com/compute/docs/disks/local-ssd" - }, - "docs-api": { - "slug": "docs-api", - "name": "Docs API", - "four_words": "Create and edit documents", - "doc_url": "https://developers.google.com/docs/api/how-tos/overview" - }, - "cloud-profiler": { - "slug": "cloud-profiler", - "name": "Cloud Profiler", - "four_words": "CPU and heap profiling", - "doc_url": "https://cloud.google.com/profiler/docs/" - }, - "security-command-center": { - "slug": "security-command-center", - "name": "Security Command Center", - "four_words": "Security management and data risk platform", - "doc_url": "https://cloud.google.com/security-command-center/docs/" - }, - "deep-learning-containers": { - "slug": "deep-learning-containers", - "name": "Deep Learning Containers", - "four_words": "Preconfigured containers for deep learning", - "doc_url": "https://cloud.google.com/deep-learning-containers/docs" - }, - "cloud-domains": { - "slug": "cloud-domains", - "name": "Cloud Domains", - "four_words": "Register, transfer, manager domains", - "doc_url": "https://cloud.google.com/domains/docs" - }, - "apps-script": { - "slug": "apps-script", - "name": "Apps Script", - "four_words": "Extend and automate everything", - "doc_url": "https://developers.google.com/apps-script/" - }, - "cloud-logging": { - "slug": "cloud-logging", - "name": "Cloud Logging", - "four_words": "Centralized logging", - "doc_url": "https://cloud.google.com/logging/docs/" - }, - "apigee-api-platform": { - "slug": "apigee-api-platform", - "name": "Apigee API Platform", - "four_words": "Develop, secure, monitor APIs", - "doc_url": "https://cloud.google.com/apigee/docs" - }, - "looker": { - "slug": "looker", - "name": "Looker", - "four_words": "Enterprise BI and Analytics", - "doc_url": "https://cloud.google.com/looker" - }, - "network-service-tiers": { - "slug": "network-service-tiers", - "name": "Network Service Tiers", - "four_words": "Price vs performance tiering", - "doc_url": "https://cloud.google.com/network-tiers/docs/" - }, - "container-registry": { - "slug": "container-registry", - "name": "Container Registry", - "four_words": "Private container registry / storage", - "doc_url": "https://cloud.google.com/container-registry/docs/" - }, - "bigquery": { - "slug": "bigquery", - "name": "BigQuery", - "four_words": "Data warehouse / analytics", - "doc_url": "https://cloud.google.com/bigquery/docs/" - }, - "data-catalog": { - "slug": "data-catalog", - "name": "Data Catalog", - "four_words": "Metadata management service", - "doc_url": "https://cloud.google.com/data-catalog/docs/" - }, - "titan-security-key": { - "slug": "titan-security-key", - "name": "Titan Security Key", - "four_words": "Two-factor authentication (2FA) device", - "doc_url": "https://cloud.google.com/titan-security-key/#section-3" - }, - "firebase-authentication": { - "slug": "firebase-authentication", - "name": "Firebase Authentication", - "four_words": "Drop-in authentication", - "doc_url": "https://firebase.google.com/docs/auth" - }, - "cloud-healthcare-api": { - "slug": "cloud-healthcare-api", - "name": "Cloud Healthcare API", - "four_words": "Healthcare system GCP interoperability", - "doc_url": "https://cloud.google.com/healthcare-api/docs" - }, - "dialogflow": { - "slug": "dialogflow", - "name": "Dialogflow", - "four_words": "Create conversational interfaces", - "doc_url": "https://cloud.google.com/dialogflow/docs" - }, - "cloud-router": { - "slug": "cloud-router", - "name": "Cloud Router", - "four_words": "VPC / on-prem network route exchange (BGP)", - "doc_url": "https://cloud.google.com/network-connectivity/docs/router" - }, - "vertex-ai-matching-engine": { - "slug": "vertex-ai-matching-engine", - "name": "Vertex AI Matching Engine", - "four_words": "Vector similarity searches", - "doc_url": "https://cloud.google.com/vertex-ai/docs/matching-engine" - }, - "task-api": { - "slug": "task-api", - "name": "Task API", - "four_words": "Search, read & update Tasks", - "doc_url": "https://developers.google.com/tasks/reference/rest" - } -} \ No newline at end of file + "duet-ai": { + "slug": "duet-ai", + "name": "Duet AI in Google Cloud", + "four_words": "AI-powered collaborator", + "doc_url": "https://cloud.google.com/duet-ai" + }, + "container-analysis": { + "slug": "container-analysis", + "name": "Container Analysis", + "four_words": "Automated security scanning", + "doc_url": "https://cloud.google.com/container-analysis/docs/container-scanning-overview" + }, + "vertex-ai-workbench": { + "slug": "vertex-ai-workbench", + "name": "Vertex AI Workbench", + "four_words": "Jupyter-based environment for Data Science", + "doc_url": "https://cloud.google.com/vertex-ai/docs/workbench" + }, + "api-gateway": { + "slug": "api-gateway", + "name": "API Gateway", + "four_words": "Fully managed API Gateway", + "doc_url": "https://cloud.google.com/api-gateway/docs" + }, + "cloud-memorystore": { + "slug": "cloud-memorystore", + "name": "Cloud Memorystore", + "four_words": "Managed Redis and Memcached", + "doc_url": "https://cloud.google.com/memorystore/docs/" + }, + "ml-kit-for-firebase": { + "slug": "ml-kit-for-firebase", + "name": "ML Kit for Firebase", + "four_words": "ML APIs for mobile", + "doc_url": "https://firebase.google.com/products/ml" + }, + "web-security-scanner": { + "slug": "web-security-scanner", + "name": "Web Security Scanner", + "four_words": "Identifies web-app security vulnerabilities", + "doc_url": "https://cloud.google.com/security-command-center/docs/concepts-web-security-scanner-overview" + }, + "traffic-director": { + "slug": "traffic-director", + "name": "Traffic Director", + "four_words": "Service mesh traffic management", + "doc_url": "https://cloud.google.com/traffic-director/docs/" + }, + "migrate-from-teradata": { + "slug": "migrate-from-teradata", + "name": "Migrate from Teradata", + "four_words": "Migrate from Teradata to BigQuery", + "doc_url": "https://cloud.google.com/architecture/dw2bq/dw-bq-migration-overview" + }, + "access-transparency": { + "slug": "access-transparency", + "name": "Access Transparency", + "four_words": "Audit cloud provider access", + "doc_url": "https://cloud.google.com/cloud-provider-access-management/access-transparency/docs/overview" + }, + "maps-sdk-for-unity": { + "slug": "maps-sdk-for-unity", + "name": "Maps SDK for Unity", + "four_words": "Unity SDK for games", + "doc_url": "https://developers.google.com/maps/documentation" + }, + "access-context-manager": { + "slug": "access-context-manager", + "name": "Access Context Manager", + "four_words": "Fine-grained, attribute based access-control", + "doc_url": "https://cloud.google.com/access-context-manager/docs" + }, + "recaptcha-enterprise": { + "slug": "recaptcha-enterprise", + "name": "reCAPTCHA Enterprise", + "four_words": "Protect website against bots", + "doc_url": "https://cloud.google.com/recaptcha-enterprise/docs" + }, + "cloud-vision": { + "slug": "cloud-vision", + "name": "Cloud Vision", + "four_words": "Image recognition and classification", + "doc_url": "https://cloud.google.com/vision/docs/" + }, + "public-datasets": { + "slug": "public-datasets", + "name": "Public Datasets", + "four_words": "Hosted data in BigQuery", + "doc_url": "https://cloud.google.com/bigquery/public-data" + }, + "cloud-tasks": { + "slug": "cloud-tasks", + "name": "Cloud Tasks", + "four_words": "Asynchronous task execution", + "doc_url": "https://cloud.google.com/tasks/docs/" + }, + "dataprep-by-trifacta": { + "slug": "dataprep-by-trifacta", + "name": "Dataprep by Trifacta", + "four_words": "Visual data wrangling", + "doc_url": "https://docs.trifacta.com/display/dp/" + }, + "appsheet": { + "slug": "appsheet", + "name": "AppSheet", + "four_words": "No-code App creation", + "doc_url": "https://cloud.google.com/appsheet" + }, + "cloud-deployment-manager": { + "slug": "cloud-deployment-manager", + "name": "Cloud Deployment Manager", + "four_words": "Templated infrastructure deployment", + "doc_url": "https://cloud.google.com/deployment-manager/docs/" + }, + "roads-api": { + "slug": "roads-api", + "name": "Roads API", + "four_words": "Convert coordinates to roads", + "doc_url": "https://developers.google.com/maps/documentation/roads/overview" + }, + "anthos-config-management": { + "slug": "anthos-config-management", + "name": "Anthos Config Management", + "four_words": "Policy and security automation", + "doc_url": "https://cloud.google.com/anthos-config-management/docs" + }, + "bigquery-data-transfer-service": { + "slug": "bigquery-data-transfer-service", + "name": "BigQuery Data Transfer Service", + "four_words": "Bulk import analytics data", + "doc_url": "https://cloud.google.com/bigquery-transfer/docs" + }, + "sole-tenant-nodes": { + "slug": "sole-tenant-nodes", + "name": "Sole-tenant Nodes", + "four_words": "Dedicated physical servers", + "doc_url": "https://cloud.google.com/compute/docs/nodes/sole-tenant-nodes" + }, + "cloud-code-for-intellij": { + "slug": "cloud-code-for-intellij", + "name": "Cloud Code for IntelliJ", + "four_words": "IntelliJ Google Cloud tools", + "doc_url": "https://cloud.google.com/code/docs" + }, + "packet-mirroring": { + "slug": "packet-mirroring", + "name": "Packet Mirroring", + "four_words": "Monitor / analyze instance traffic", + "doc_url": "https://cloud.google.com/vpc/docs/packet-mirroring" + }, + "cloud-armor": { + "slug": "cloud-armor", + "name": "Cloud Armor", + "four_words": "DDoS protection and WAF", + "doc_url": "https://cloud.google.com/armor/docs/" + }, + "cloud-deploy": { + "slug": "cloud-deploy", + "name": "Cloud Deploy", + "four_words": "Continuous Delivery for GKE", + "doc_url": "https://cloud.google.com/deploy/docs" + }, + "cloud-bigtable": { + "slug": "cloud-bigtable", + "name": "Cloud Bigtable", + "four_words": "Petabyte-scale, low-latency, non-relational", + "doc_url": "https://cloud.google.com/bigtable/docs/" + }, + "anthos-service-mesh": { + "slug": "anthos-service-mesh", + "name": "Anthos Service Mesh", + "four_words": "Managed service mesh (Istio)", + "doc_url": "https://cloud.google.com/service-mesh/docs" + }, + "cloud-trace": { + "slug": "cloud-trace", + "name": "Cloud Trace", + "four_words": "App latency insights", + "doc_url": "https://cloud.google.com/trace/docs/" + }, + "healthcare-natural-language-ai": { + "slug": "healthcare-natural-language-ai", + "name": "Healthcare Natural Language AI", + "four_words": "Real-time insights from media-text", + "doc_url": "https://cloud.google.com/healthcare-api/docs/how-tos/nlp" + }, + "migrate-from-amazon-redshift": { + "slug": "migrate-from-amazon-redshift", + "name": "Migrate from Amazon Redshift", + "four_words": "Migrate from Redshift to BigQuery", + "doc_url": "https://cloud.google.com/bigquery-transfer/docs/redshift-migration" + }, + "maps-sdk-for-android": { + "slug": "maps-sdk-for-android", + "name": "Maps SDK for Android", + "four_words": "Maps for Android apps", + "doc_url": "https://developers.google.com/maps/documentation/android-sdk/overview" + }, + "cloud-shell": { + "slug": "cloud-shell", + "name": "Cloud Shell", + "four_words": "Browser-based terminal / CLI", + "doc_url": "https://cloud.google.com/shell/docs/" + }, + "pubsub": { + "slug": "pubsub", + "name": "Pub/Sub", + "four_words": "Global real-time messaging", + "doc_url": "https://cloud.google.com/pubsub/docs/" + }, + "network-connectivity-center": { + "slug": "network-connectivity-center", + "name": "Network Connectivity Center", + "four_words": "Connect VPC & On-prem", + "doc_url": "https://cloud.google.com/network-connectivity/docs/network-connectivity-center/concepts/overview" + }, + "document-ai": { + "slug": "document-ai", + "name": "Document AI", + "four_words": "Analyze, classify, search documents", + "doc_url": "https://cloud.google.com/document-ai/docs" + }, + "dataplex": { + "slug": "dataplex", + "name": "Dataplex", + "four_words": "Centrally manage / monitor / govern data", + "doc_url": "https://cloud.google.com/dataplex" + }, + "firebase-test-lab": { + "slug": "firebase-test-lab", + "name": "Firebase Test Lab", + "four_words": "Mobile testing device farm", + "doc_url": "https://firebase.google.com/docs/test-lab/" + }, + "geocoding-api": { + "slug": "geocoding-api", + "name": "Geocoding API", + "four_words": "Convert address to/from coordinates", + "doc_url": "https://developers.google.com/maps/documentation/geocoding/overview" + }, + "cloud-functions": { + "slug": "cloud-functions", + "name": "Cloud Functions", + "four_words": "Event-driven serverless functions", + "doc_url": "https://cloud.google.com/functions/docs/" + }, + "security-key-enforcement": { + "slug": "security-key-enforcement", + "name": "Security Key Enforcement", + "four_words": "Two-step key verification", + "doc_url": "https://cloud.google.com/titan-security-key" + }, + "identity-platform": { + "slug": "identity-platform", + "name": "Identity Platform", + "four_words": "Drop-in Authentication, Access - Management", + "doc_url": "https://cloud.google.com/identity-platform/docs" + }, + "cloud-composer": { + "slug": "cloud-composer", + "name": "Cloud Composer", + "four_words": "Managed workflow orchestration service", + "doc_url": "https://cloud.google.com/composer/docs/" + }, + "cloud-identity": { + "slug": "cloud-identity", + "name": "Cloud Identity", + "four_words": "Manage users, devices & apps", + "doc_url": "https://cloud.google.com/identity/solutions/overview/" + }, + "assured-workloads": { + "slug": "assured-workloads", + "name": "Assured Workloads", + "four_words": "Workload compliance controls", + "doc_url": "https://cloud.google.com/assured-workloads/docs" + }, + "service-directory": { + "slug": "service-directory", + "name": "Service Directory", + "four_words": "Centrally publish / discover / connect services", + "doc_url": "https://cloud.google.com/service-directory/docs" + }, + "cloud-tools-for-eclipse": { + "slug": "cloud-tools-for-eclipse", + "name": "Cloud Tools for Eclipse", + "four_words": "Eclipse GCP tools", + "doc_url": "https://cloud.google.com/eclipse/docs/" + }, + "cloud-spanner": { + "slug": "cloud-spanner", + "name": "Cloud Spanner", + "four_words": "Horizontally scaleable relational database", + "doc_url": "https://cloud.google.com/spanner/docs/" + }, + "recommendations-ai": { + "slug": "recommendations-ai", + "name": "Recommendations AI", + "four_words": "Create custom recommendations", + "doc_url": "https://cloud.google.com/recommendations-ai/docs/" + }, + "cloud-apis": { + "slug": "cloud-apis", + "name": "Cloud APIs", + "four_words": "APIs for cloud services", + "doc_url": "https://cloud.google.com/apis/docs/overview" + }, + "apigee-healthcare-apix": { + "slug": "apigee-healthcare-apix", + "name": "Apigee Healthcare APIx", + "four_words": "Healthcare system GCP interoperability", + "doc_url": "https://cloud.google.com/solutions/apigee-health-apix#section-4" + }, + "email-markup": { + "slug": "email-markup", + "name": "Email Markup", + "four_words": "Interactive email using schema.org", + "doc_url": "https://developers.google.com/gmail/markup/overview" + }, + "eventarc": { + "slug": "eventarc", + "name": "Eventarc", + "four_words": "Event-driven Cloud Run services", + "doc_url": "https://cloud.google.com/eventarc/docs/" + }, + "vpc-service-controls": { + "slug": "vpc-service-controls", + "name": "VPC Service Controls", + "four_words": "VPC data constraints", + "doc_url": "https://cloud.google.com/vpc-service-controls/docs/" + }, + "vertex-ai-training": { + "slug": "vertex-ai-training", + "name": "Vertex AI Training", + "four_words": "Distributed AI training", + "doc_url": "https://cloud.google.com/ai-platform/training/docs/overview" + }, + "automl": { + "slug": "automl", + "name": "AutoML", + "four_words": "Custom low-code models", + "doc_url": "https://cloud.google.com/vertex-ai/docs/start/automl-users" + }, + "connected-sheets": { + "slug": "connected-sheets", + "name": "Connected Sheets", + "four_words": "Spreadsheet interface for (big) data", + "doc_url": "https://cloud.google.com/bigquery/docs/connected-sheets" + }, + "preemptible-vms": { + "slug": "preemptible-vms", + "name": "Preemptible VMs", + "four_words": "Short-lived compute instances", + "doc_url": "https://cloud.google.com/compute/docs/instances/preemptible" + }, + "apigee-sense": { + "slug": "apigee-sense", + "name": "Apigee Sense", + "four_words": "API protection from attacks", + "doc_url": "https://cloud.google.com/apigee/sense" + }, + "cloud-ids": { + "slug": "cloud-ids", + "name": "Cloud IDS", + "four_words": "Detects Network based threats", + "doc_url": "https://cloud.google.com/intrusion-detection-system/docs" + }, + "firebase-remote-config": { + "slug": "firebase-remote-config", + "name": "Firebase Remote Config", + "four_words": "Remotely configure installed apps", + "doc_url": "https://firebase.google.com/docs/remote-config/" + }, + "vertex-ai": { + "slug": "vertex-ai", + "name": "Vertex AI", + "four_words": "Managed platform for ML", + "doc_url": "https://cloud.google.com/vertex-ai/docs" + }, + "directions-api": { + "slug": "directions-api", + "name": "Directions API", + "four_words": "Get directions between locations", + "doc_url": "https://developers.google.com/maps/documentation/directions/overview" + }, + "apigee-api-management": { + "slug": "apigee-api-management", + "name": "Apigee API Management", + "four_words": "API management, development, security", + "doc_url": "https://cloud.google.com/apigee/docs" + }, + "gmail-api": { + "slug": "gmail-api", + "name": "Gmail API", + "four_words": "Enhance Gmail", + "doc_url": "https://developers.google.com/gmail/api" + }, + "cloud-firestore": { + "slug": "cloud-firestore", + "name": "Cloud Firestore", + "four_words": "Serverless NoSQL document DB", + "doc_url": "https://cloud.google.com/firestore/docs/" + }, + "deep-learning-vm-images": { + "slug": "deep-learning-vm-images", + "name": "Deep Learning VM Images ", + "four_words": "Preconfigured VMs for deep learning", + "doc_url": "https://cloud.google.com/deep-learning-vm/docs/" + }, + "maps-urls": { + "slug": "maps-urls", + "name": "Maps URLs", + "four_words": "URL scheme for maps", + "doc_url": "https://developers.google.com/maps/documentation/urls/get-started" + }, + "binary-authorization": { + "slug": "binary-authorization", + "name": "Binary Authorization", + "four_words": "Kubernetes deploy-time security", + "doc_url": "https://cloud.google.com/binary-authorization/docs/" + }, + "vertex-ai-predictions": { + "slug": "vertex-ai-predictions", + "name": "Vertex AI Predictions", + "four_words": "Autoscaled model serving", + "doc_url": "https://cloud.google.com/vertex-ai/docs/" + }, + "google-cloud-marketplace": { + "slug": "google-cloud-marketplace", + "name": "Google Cloud Marketplace", + "four_words": "Partner & open source marketplace", + "doc_url": "https://cloud.google.com/marketplace/docs" + }, + "contact-center-ai": { + "slug": "contact-center-ai", + "name": "Contact Center AI", + "four_words": "AI in your contact center", + "doc_url": "https://cloud.google.com/solutions/contact-center" + }, + "dataproc": { + "slug": "dataproc", + "name": "Dataproc", + "four_words": "Managed Spark and Hadoop", + "doc_url": "https://cloud.google.com/dataproc/docs/" + }, + "maps-sdk-for-ios": { + "slug": "maps-sdk-for-ios", + "name": "Maps SDK for iOS", + "four_words": "Maps for iOS apps", + "doc_url": "https://developers.google.com/maps/documentation/ios-sdk/overview" + }, + "cloud-sql": { + "slug": "cloud-sql", + "name": "Cloud SQL", + "four_words": "Managed MySQL, PostgreSQL, SQL Server", + "doc_url": "https://cloud.google.com/sql/docs/" + }, + "maps-embed-api": { + "slug": "maps-embed-api", + "name": "Maps Embed API", + "four_words": "Display iframe embedded maps", + "doc_url": "https://developers.google.com/maps/documentation/embed/get-started" + }, + "distance-matrix-api": { + "slug": "distance-matrix-api", + "name": "Distance Matrix API", + "four_words": "Multi-origin / destination travel times", + "doc_url": "https://developers.google.com/maps/documentation/distance-matrix/start" + }, + "cloud-data-loss-prevention": { + "slug": "cloud-data-loss-prevention", + "name": "Cloud Data Loss Prevention", + "four_words": "Classify and redact sensitive data", + "doc_url": "https://cloud.google.com/dlp/docs/" + }, + "firebase-extensions": { + "slug": "firebase-extensions", + "name": "Firebase Extensions", + "four_words": "Pre-packaged development solutions", + "doc_url": "https://firebase.google.com/docs/extensions" + }, + "cloud-data-transfer": { + "slug": "cloud-data-transfer", + "name": "Cloud Data Transfer", + "four_words": "Data migration tools/CLI", + "doc_url": "https://cloud.google.com/storage-transfer/docs" + }, + "google-cloud-marketplace-for-anthos": { + "slug": "google-cloud-marketplace-for-anthos", + "name": "Google Cloud Marketplace for Anthos", + "four_words": "Pre-configured containerized apps", + "doc_url": "https://cloud.google.com/marketplace/docs/kubernetes-apps" + }, + "api-monetization": { + "slug": "api-monetization", + "name": "API Monetization", + "four_words": "Monetize APIs", + "doc_url": "https://cloud.google.com/apigee/docs" + }, + "cloud-run": { + "slug": "cloud-run", + "name": "Cloud Run", + "four_words": "Serverless for containerized applications", + "doc_url": "https://cloud.google.com/run/docs" + }, + "transcoder-api": { + "slug": "transcoder-api", + "name": "Transcoder API", + "four_words": "Optimized files for delivery", + "doc_url": "https://cloud.google.com/transcoder/docs" + }, + "firebase-performance-monitoring": { + "slug": "firebase-performance-monitoring", + "name": "Firebase Performance Monitoring", + "four_words": "App/web performance monitoring", + "doc_url": "https://firebase.google.com/docs/perf-mon" + }, + "firebase-ab-testing": { + "slug": "firebase-ab-testing", + "name": "Firebase A/B Testing", + "four_words": "Create A/B test experiments", + "doc_url": "https://firebase.google.com/docs/ab-testing" + }, + "cloud-code-for-vs-code": { + "slug": "cloud-code-for-vs-code", + "name": "Cloud Code for VS Code", + "four_words": "VS Code Google Cloud tools", + "doc_url": "https://cloud.google.com/code/docs/vscode/" + }, + "firebase-cloud-messaging": { + "slug": "firebase-cloud-messaging", + "name": "Firebase Cloud Messaging", + "four_words": "Send device notifications", + "doc_url": "https://firebase.google.com/docs/cloud-messaging" + }, + "vertex-ai-pipelines": { + "slug": "vertex-ai-pipelines", + "name": "Vertex AI Pipelines", + "four_words": "Hosted ML workflows", + "doc_url": "https://cloud.google.com/ai-platform/pipelines/docs" + }, + "workflows": { + "slug": "workflows", + "name": "Workflows", + "four_words": "HTTP services orchestration", + "doc_url": "https://cloud.google.com/workflows/docs/" + }, + "anthos-clusters": { + "slug": "anthos-clusters", + "name": "Anthos Clusters", + "four_words": "Hybrid / on-prem GKE", + "doc_url": "https://cloud.google.com/anthos/clusters/docs/on-prem/1.9" + }, + "crashlytics": { + "slug": "crashlytics", + "name": "Crashlytics", + "four_words": "Crash reporting and analytics", + "doc_url": "https://firebase.google.com/docs/crashlytics" + }, + "calendar-api": { + "slug": "calendar-api", + "name": "Calendar API", + "four_words": "Create and manage calendars", + "doc_url": "https://developers.google.com/apps-script/add-ons/calendar" + }, + "visual-inspection-ai": { + "slug": "visual-inspection-ai", + "name": "Visual Inspection AI", + "four_words": "Train / deploy models to detect defects", + "doc_url": "https://cloud.google.com/solutions/visual-inspection-ai" + }, + "cloud-mobile-app": { + "slug": "cloud-mobile-app", + "name": "Cloud Mobile App", + "four_words": "iOS / Android Google Cloud manager app", + "doc_url": "https://cloud.google.com/console-app/" + }, + "cloud-translation": { + "slug": "cloud-translation", + "name": "Cloud Translation", + "four_words": "Language detection and translation", + "doc_url": "https://cloud.google.com/translate/docs/" + }, + "firebase-dynamic-links": { + "slug": "firebase-dynamic-links", + "name": "Firebase Dynamic Links", + "four_words": "Link to app content", + "doc_url": "https://firebase.google.com/docs/dynamic-links" + }, + "migrate-for-anthos-and-gke": { + "slug": "migrate-for-anthos-and-gke", + "name": "Migrate for Anthos and GKE", + "four_words": "Migrate VMs to GKE", + "doc_url": "https://cloud.google.com/migrate/anthos/docs/getting-started" + }, + "sheets-api": { + "slug": "sheets-api", + "name": "Sheets API", + "four_words": "Read and write spreadsheets", + "doc_url": "https://developers.google.com/sheets/api/reference/rest" + }, + "alloydb": { + "slug": "alloydb", + "name": "AlloyDB", + "four_words": "Scalable & performant PostgreSQL - compatible DB", + "doc_url": "https://cloud.google.com/alloydb/docs" + }, + "app-engine": { + "slug": "app-engine", + "name": "App Engine", + "four_words": "Managed app platform", + "doc_url": "https://cloud.google.com/appengine/docs/" + }, + "time-zone-api": { + "slug": "time-zone-api", + "name": "Time Zone API", + "four_words": "Convert coordinates to timezone", + "doc_url": "https://developers.google.com/maps/documentation/timezone/get-started" + }, + "video-intelligence-api": { + "slug": "video-intelligence-api", + "name": "Video Intelligence API", + "four_words": "Scene-level video annotation", + "doc_url": "https://cloud.google.com/video-intelligence/docs/" + }, + "private-catalog": { + "slug": "private-catalog", + "name": "Private Catalog", + "four_words": "Internal Solutions Catalog", + "doc_url": "https://cloud.google.com/private-catalog/docs/" + }, + "cloud-load-balancing": { + "slug": "cloud-load-balancing", + "name": "Cloud Load Balancing", + "four_words": "Multi-region load distribution / balancing", + "doc_url": "https://cloud.google.com/load-balancing/docs" + }, + "bigquery-dts": { + "slug": "bigquery-dts", + "name": "BigQuery DTS", + "four_words": "Automated data ingestion service", + "doc_url": "https://cloud.google.com/bigquery-transfer/docs" + }, + "vertex-ai-data-labeling": { + "slug": "vertex-ai-data-labeling", + "name": "Vertex AI Data Labeling", + "four_words": "Data labeling by humans", + "doc_url": "https://cloud.google.com/ai-platform/data-labeling/docs" + }, + "chronicle": { + "slug": "chronicle", + "name": "Chronicle", + "four_words": "Find threats from security telemetry", + "doc_url": "https://cloud.google.com/chronicle/docs" + }, + "api-analytics": { + "slug": "api-analytics", + "name": "API Analytics", + "four_words": "API metrics", + "doc_url": "https://cloud.google.com/apigee/docs" + }, + "shielded-vms": { + "slug": "shielded-vms", + "name": "Shielded VMs", + "four_words": "Hardened VMs", + "doc_url": "https://cloud.google.com/security/shielded-cloud/shielded-vm/" + }, + "places-sdk-for-ios": { + "slug": "places-sdk-for-ios", + "name": "Places SDK for iOS", + "four_words": "Places feature for iOS", + "doc_url": "https://developers.google.com/maps/documentation/places/ios-sdk/start" + }, + "cloud-sdk": { + "slug": "cloud-sdk", + "name": "Cloud SDK", + "four_words": "Google Cloud CLI", + "doc_url": "https://cloud.google.com/sdk/docs/" + }, + "virustotal": { + "slug": "virustotal", + "name": "VirusTotal", + "four_words": "Research / hunt for malware", + "doc_url": "https://cloud.google.com/chronicle/docs/investigation/view-virustotal-information" + }, + "anthos": { + "slug": "anthos", + "name": "Anthos", + "four_words": "Enterprise hybrid / multi-cloud platform", + "doc_url": "https://cloud.google.com/anthos/docs/" + }, + "dedicated-interconnect": { + "slug": "dedicated-interconnect", + "name": "Dedicated Interconnect", + "four_words": "Dedicated private network connection", + "doc_url": "https://cloud.google.com/network-connectivity/docs/interconnect/concepts/dedicated-overview" + }, + "cloud-run-for-anthos": { + "slug": "cloud-run-for-anthos", + "name": "Cloud Run for Anthos", + "four_words": "Serverless development for Anthos", + "doc_url": "https://cloud.google.com/anthos/run/docs/quickstarts/prebuilt-deploy-gke" + }, + "cloud-nat": { + "slug": "cloud-nat", + "name": "Cloud NAT", + "four_words": "Network address translation service", + "doc_url": "https://cloud.google.com/nat/docs" + }, + "gke": { + "slug": "gke", + "name": "Kubernetes Engine", + "four_words": "Managed Kubernetes / containers", + "doc_url": "https://cloud.google.com/kubernetes-engine/docs/" + }, + "media-cdn": { + "slug": "media-cdn", + "name": "Media CDN", + "four_words": "CDN for streaming & videos", + "doc_url": "https://cloud.google.com/media-cdn/docs/overview" + }, + "cloud-storage-for-firebase": { + "slug": "cloud-storage-for-firebase", + "name": "Cloud Storage for Firebase", + "four_words": "Object storage and serving", + "doc_url": "https://firebase.google.com/docs/storage" + }, + "admin-sdk": { + "slug": "admin-sdk", + "name": "Admin SDK", + "four_words": "Manage Google Workspace resources", + "doc_url": "https://developers.google.com/apps-script/advanced/admin-sdk-directory" + }, + "google-analytics-for-firebase": { + "slug": "google-analytics-for-firebase", + "name": "Google Analytics for Firebase", + "four_words": "Mobile app analytics", + "doc_url": "https://firebase.google.com/docs/analytics" + }, + "vm-manager": { + "slug": "vm-manager", + "name": "VM Manager", + "four_words": "Manage OS VM Fleets", + "doc_url": "https://cloud.google.com/compute/docs/vm-manager" + }, + "error-reporting": { + "slug": "error-reporting", + "name": "Error Reporting", + "four_words": "App error reporting", + "doc_url": "https://cloud.google.com/error-reporting/docs/" + }, + "beyondcorp-enterprise": { + "slug": "beyondcorp-enterprise", + "name": "BeyondCorp Enterprise", + "four_words": "Zero trust secure access", + "doc_url": "https://cloud.google.com/beyondcorp-enterprise/docs" + }, + "dataflow": { + "slug": "dataflow", + "name": "Dataflow", + "four_words": "Stream / batch data processing", + "doc_url": "https://cloud.google.com/dataflow/docs/" + }, + "risk-manager": { + "slug": "risk-manager", + "name": "Risk Manager", + "four_words": "Evaluate organization's security posture", + "doc_url": "https://cloud.google.com/risk-manager/docs" + }, + "kf": { + "slug": "kf", + "name": "KF", + "four_words": "Cloud Foundry to Kubernetes", + "doc_url": "https://cloud.google.com/migrate/kf/docs/2.6" + }, + "vision-product-search": { + "slug": "vision-product-search", + "name": "Vision Product Search", + "four_words": "Visual search for products", + "doc_url": "https://cloud.google.com/vision/product-search/docs/" + }, + "carrier-peering": { + "slug": "carrier-peering", + "name": "Carrier Peering", + "four_words": "Peer through a carrier", + "doc_url": "https://cloud.google.com/network-connectivity/docs/carrier-peering" + }, + "private-service-connect": { + "slug": "private-service-connect", + "name": "Private Service Connect", + "four_words": "Privately connect services across VPCs", + "doc_url": "https://cloud.google.com/vpc/docs/private-service-connect" + }, + "cloud-audit-logs": { + "slug": "cloud-audit-logs", + "name": "Cloud Audit Logs", + "four_words": "Audit trails", + "doc_url": "https://cloud.google.com/logging/docs/audit/" + }, + "vertex-explainable-ai": { + "slug": "vertex-explainable-ai", + "name": "Vertex Explainable AI", + "four_words": "Understand ML model predictions", + "doc_url": "https://cloud.google.com/vertex-ai/docs/explainable-ai" + }, + "places-api": { + "slug": "places-api", + "name": "Places API", + "four_words": "Rest-based Places features", + "doc_url": "https://developers.google.com/maps/documentation/places/web-service/overview" + }, + "vault-api": { + "slug": "vault-api", + "name": "Vault API", + "four_words": "Manage your organization's eDiscovery", + "doc_url": "https://developers.google.com/vault/reference/rest" + }, + "managed-service-for-microsoft-active-directory": { + "slug": "managed-service-for-microsoft-active-directory", + "name": "Managed Service for Microsoft Active Directory", + "four_words": "Managed Microsoft Active Directory", + "doc_url": "https://cloud.google.com/managed-microsoft-ad/docs/" + }, + "bigquery-gis": { + "slug": "bigquery-gis", + "name": "BigQuery GIS", + "four_words": "BigQuery geospatial functions / support", + "doc_url": "https://cloud.google.com/bigquery/docs/geospatial-intro" + }, + "cloud-iam": { + "slug": "cloud-iam", + "name": "Cloud IAM", + "four_words": "Resource access control", + "doc_url": "https://cloud.google.com/iam/docs/" + }, + "cloud-cdn": { + "slug": "cloud-cdn", + "name": "Cloud CDN", + "four_words": "Content delivery network", + "doc_url": "https://cloud.google.com/cdn/docs/" + }, + "app-engine-plugins": { + "slug": "app-engine-plugins", + "name": "App Engine Plugins", + "four_words": "Gradle / Maven App Engine plugin", + "doc_url": "https://cloud.google.com/appengine/docs/standard/java/gradle-reference" + }, + "vertex-ai-vizier": { + "slug": "vertex-ai-vizier", + "name": "Vertex AI Vizier", + "four_words": "Automated hyperparameter tuning", + "doc_url": "https://cloud.google.com/vertex-ai/docs/vizier" + }, + "firebase-realtime-database": { + "slug": "firebase-realtime-database", + "name": "Firebase Realtime Database", + "four_words": "Real-time data synchronization", + "doc_url": "https://firebase.google.com/docs/database" + }, + "certificate-authority-service": { + "slug": "certificate-authority-service", + "name": "Certificate Authority Service", + "four_words": "Managed private CAs", + "doc_url": "https://cloud.google.com/certificate-authority-service/docs" + }, + "bigquery-bi-engine": { + "slug": "bigquery-bi-engine", + "name": "BigQuery BI Engine", + "four_words": "In-memory analytics engine", + "doc_url": "https://cloud.google.com/bi-engine/docs/" + }, + "virtual-private-cloud": { + "slug": "virtual-private-cloud", + "name": "Virtual Private Cloud", + "four_words": "Software defined networking", + "doc_url": "https://cloud.google.com/vpc/docs/" + }, + "firebase-in-app-messaging": { + "slug": "firebase-in-app-messaging", + "name": "Firebase In-App Messaging", + "four_words": "Send in-app contextual messages", + "doc_url": "https://firebase.google.com/docs/in-app-messaging" + }, + "drive-activity-api": { + "slug": "drive-activity-api", + "name": "Drive Activity API", + "four_words": "Retrieve Google Drive activity", + "doc_url": "https://developers.google.com/drive/activity/" + }, + "google-chat-api": { + "slug": "google-chat-api", + "name": "Google Chat API", + "four_words": "Conversational bots in chat", + "doc_url": "https://developers.google.com/chat" + }, + "cloud-code": { + "slug": "cloud-code", + "name": "Cloud Code", + "four_words": "Google Cloud IDE extensions", + "doc_url": "https://cloud.google.com/code/docs/" + }, + "speech-to-text": { + "slug": "speech-to-text", + "name": "Speech-To-Text", + "four_words": "Convert audio to text", + "doc_url": "https://cloud.google.com/speech-to-text/docs" + }, + "firebase-hosting": { + "slug": "firebase-hosting", + "name": "Firebase Hosting", + "four_words": "Web hosting with CDN/SSL", + "doc_url": "https://firebase.google.com/docs/hosting" + }, + "vertex-ai-feature-store": { + "slug": "vertex-ai-feature-store", + "name": "Vertex AI Feature Store", + "four_words": "Managed ML feature repository", + "doc_url": "https://cloud.google.com/vertex-ai/docs/featurestore" + }, + "places-sdk-for-android": { + "slug": "places-sdk-for-android", + "name": "Places SDK for Android", + "four_words": "Places features for Android", + "doc_url": "https://developers.google.com/maps/documentation/places/android-sdk/start" + }, + "firebase-predictions": { + "slug": "firebase-predictions", + "name": "Firebase Predictions", + "four_words": "Predict user targeting", + "doc_url": "https://firebase.google.com/docs/predictions" + }, + "resource-manager": { + "slug": "resource-manager", + "name": "Resource Manager", + "four_words": "Cloud project metadata management", + "doc_url": "https://cloud.google.com/resource-manager/docs" + }, + "cloud-filestore": { + "slug": "cloud-filestore", + "name": "Cloud Filestore", + "four_words": "Managed NFS server", + "doc_url": "https://cloud.google.com/filestore/docs/" + }, + "developer-portal": { + "slug": "developer-portal", + "name": "Developer Portal", + "four_words": "API management portal", + "doc_url": "https://cloud.google.com/apigee/docs" + }, + "cloud-console": { + "slug": "cloud-console", + "name": "Cloud Console", + "four_words": "Web-based management console", + "doc_url": "https://cloud.google.com/cloud-console/" + }, + "migrate-for-anthos": { + "slug": "migrate-for-anthos", + "name": "Migrate for Anthos", + "four_words": "Migrate VMs to Kubernetes Engine", + "doc_url": "https://cloud.google.com/migrate/anthos/docs/getting-started" + }, + "datastream": { + "slug": "datastream", + "name": "Datastream", + "four_words": "Change data capture / replication service", + "doc_url": "https://cloud.google.com/datastream/docs" + }, + "cloud-ekm": { + "slug": "cloud-ekm", + "name": "Cloud EKM", + "four_words": "External keys you control / manage", + "doc_url": "https://cloud.google.com/kms/docs/ekm/" + }, + "google-cloud-game-servers": { + "slug": "google-cloud-game-servers", + "name": "Google Cloud Game Servers", + "four_words": "Orchestrate Agones clusters", + "doc_url": "https://cloud.google.com/game-servers/docs" + }, + "apigee-hybrid": { + "slug": "apigee-hybrid", + "name": "Apigee Hybrid", + "four_words": "Manage hybrid / multi-cloud API environments", + "doc_url": "https://cloud.google.com/apigee/docs/hybrid/v1.6/what-is-hybrid" + }, + "street-view-static-api": { + "slug": "street-view-static-api", + "name": "Street View Static API", + "four_words": "Static street view images", + "doc_url": "https://developers.google.com/maps/documentation/streetview/overview" + }, + "context-aware-access": { + "slug": "context-aware-access", + "name": "Context-aware Access", + "four_words": "End-user attribute-based access control", + "doc_url": "https://cloud.google.com/iap/docs/cloud-iap-context-aware-access-howto/" + }, + "vmware-engine": { + "slug": "vmware-engine", + "name": "VMware Engine", + "four_words": "VMware as a service", + "doc_url": "https://cloud.google.com/vmware-engine/docs" + }, + "maps-static-api": { + "slug": "maps-static-api", + "name": "Maps Static API", + "four_words": "Display static map images", + "doc_url": "https://developers.google.com/maps/documentation/maps-static/start" + }, + "cloud-foundation-toolkit": { + "slug": "cloud-foundation-toolkit", + "name": "Cloud Foundation Toolkit", + "four_words": "Infrastructure as Code templates", + "doc_url": "https://github.com/GoogleCloudPlatform/cloud-foundation-toolkit/blob/master/docs/terraform.md" + }, + "drive-picker": { + "slug": "drive-picker", + "name": "Drive Picker", + "four_words": "Drive file selection widget", + "doc_url": "https://developers.google.com/picker/docs/reference" + }, + "drive-api": { + "slug": "drive-api", + "name": "Drive API", + "four_words": "Read and write files", + "doc_url": "https://developers.google.com/apps-script/reference/drive" + }, + "vertex-ai-model-monitoring": { + "slug": "vertex-ai-model-monitoring", + "name": "Vertex AI Model Monitoring", + "four_words": "Monitor models for skew/drift", + "doc_url": "https://cloud.google.com/vertex-ai/docs/model-monitoring" + }, + "database-migration-service": { + "slug": "database-migration-service", + "name": "Database Migration Service", + "four_words": "Migrate to Cloud SQL", + "doc_url": "https://cloud.google.com/database-migration/docs" + }, + "secret-manager": { + "slug": "secret-manager", + "name": "Secret Manager", + "four_words": "Store and manage secrets", + "doc_url": "https://cloud.google.com/secret-manager/docs/" + }, + "google-workspace-marketplace": { + "slug": "google-workspace-marketplace", + "name": "Google Workspace Marketplace", + "four_words": "Storefront for integrated applications", + "doc_url": "https://developers.google.com/workspace/marketplace/reference/rest" + }, + "vertex-ml-metadata": { + "slug": "vertex-ml-metadata", + "name": "Vertex ML Metadata", + "four_words": "Artifact, lineage, and execution tracking", + "doc_url": "https://cloud.google.com/vertex-ai/docs/ml-metadata" + }, + "cloud-endpoints": { + "slug": "cloud-endpoints", + "name": "Cloud Endpoints", + "four_words": "Cloud API gateway", + "doc_url": "https://cloud.google.com/endpoints/docs" + }, + "firebase-app-distribution": { + "slug": "firebase-app-distribution", + "name": "Firebase App Distribution", + "four_words": "Trusted tester early access", + "doc_url": "https://firebase.google.com/docs/app-distribution" + }, + "network-telemetry": { + "slug": "network-telemetry", + "name": "Network Telemetry", + "four_words": "Network telemetry service", + "doc_url": "https://cloud.google.com/vpc/docs/using-flow-logs/" + }, + "cloud-identity-aware-proxy": { + "slug": "cloud-identity-aware-proxy", + "name": "Cloud Identity-Aware Proxy", + "four_words": "Identity-based app access", + "doc_url": "https://cloud.google.com/iap/docs/" + }, + "transfer-appliance": { + "slug": "transfer-appliance", + "name": "Transfer Appliance", + "four_words": "Rentable data transport box", + "doc_url": "https://cloud.google.com/transfer-appliance/docs/4.0" + }, + "maps-javascript-api": { + "slug": "maps-javascript-api", + "name": "Maps JavaScript API", + "four_words": "Dynamic web maps", + "doc_url": "https://developers.google.com/maps/documentation/javascript/overview" + }, + "cloud-scheduler": { + "slug": "cloud-scheduler", + "name": "Cloud Scheduler", + "four_words": "Managed cron job service", + "doc_url": "https://cloud.google.com/scheduler/docs/" + }, + "storage-transfer-service": { + "slug": "storage-transfer-service", + "name": "Storage Transfer Service", + "four_words": "Online / on-premises data transfer", + "doc_url": "https://cloud.google.com/storage-transfer/docs" + }, + "network-intelligence-center": { + "slug": "network-intelligence-center", + "name": "Network Intelligence Center", + "four_words": "Network monitoring and topology", + "doc_url": "https://cloud.google.com/network-intelligence-center/docs/" + }, + "persistent-disk": { + "slug": "persistent-disk", + "name": "Persistent Disk", + "four_words": "Block storage for VMs", + "doc_url": "https://cloud.google.com/compute/docs/disks/" + }, + "google-workspace-add-ons": { + "slug": "google-workspace-add-ons", + "name": "Google Workspace Add-ons", + "four_words": "Extend Google Workspace apps", + "doc_url": "https://developers.google.com/apps-script/add-ons/overview" + }, + "cloud-storage": { + "slug": "cloud-storage", + "name": "Cloud Storage", + "four_words": "Multi-class multi-region object storage", + "doc_url": "https://cloud.google.com/storage/docs/" + }, + "cloud-life-sciences": { + "slug": "cloud-life-sciences", + "name": "Cloud Life Sciences", + "four_words": "Manage, process, transform biomedical-data", + "doc_url": "https://cloud.google.com/life-sciences/docs" + }, + "street-view-service": { + "slug": "street-view-service", + "name": "Street View Service", + "four_words": "Street view for JavaScript", + "doc_url": "https://developers.google.com/maps/documentation/javascript/streetview/" + }, + "bigquery-ml": { + "slug": "bigquery-ml", + "name": "BigQuery ML", + "four_words": "BigQuery model training / serving", + "doc_url": "https://cloud.google.com/bigquery-ml/docs/" + }, + "partner-interconnect": { + "slug": "partner-interconnect", + "name": "Partner Interconnect", + "four_words": "Connect on-prem network to VPC", + "doc_url": "https://cloud.google.com/network-connectivity/docs/interconnect/concepts/partner-overview" + }, + "cloud-hsm": { + "slug": "cloud-hsm", + "name": "Cloud HSM", + "four_words": "Hardware security module service", + "doc_url": "https://cloud.google.com/kms/docs/hsm/" + }, + "migrate-for-compute-engine": { + "slug": "migrate-for-compute-engine", + "name": "Migrate for Compute Engine", + "four_words": "Compute Engine migration tools", + "doc_url": "https://cloud.google.com/migrate/compute-engine/docs/5.0" + }, + "slides-api": { + "slug": "slides-api", + "name": "Slides API", + "four_words": "Create and edit presentations", + "doc_url": "https://developers.google.com/slides/api" + }, + "people-api": { + "slug": "people-api", + "name": "People API", + "four_words": "Manage user's Contacts", + "doc_url": "https://developers.google.com/people/api/rest" + }, + "artifact-registry": { + "slug": "artifact-registry", + "name": "Artifact Registry", + "four_words": "Universal package manager", + "doc_url": "https://cloud.google.com/artifact-registry/docs" + }, + "cloud-data-fusion": { + "slug": "cloud-data-fusion", + "name": "Cloud Data Fusion", + "four_words": "Graphically manage data pipelines", + "doc_url": "https://cloud.google.com/data-fusion/docs/" + }, + "cloud-sql-insights": { + "slug": "cloud-sql-insights", + "name": "Cloud SQL Insights", + "four_words": "SQL Inspector", + "doc_url": "https://cloud.google.com/sql/docs/postgres/using-query-insights" + }, + "cloud-billing": { + "slug": "cloud-billing", + "name": "Cloud Billing", + "four_words": "Billing and cost management tools", + "doc_url": "https://cloud.google.com/billing/docs/" + }, + "cloud-source-repositories": { + "slug": "cloud-source-repositories", + "name": "Cloud Source Repositories", + "four_words": "Hosted private git repos", + "doc_url": "https://cloud.google.com/source-repositories/docs/" + }, + "compute-engine": { + "slug": "compute-engine", + "name": "Compute Engine", + "four_words": "VMs, GPUs, TPUs, Disks", + "doc_url": "https://cloud.google.com/compute/docs/" + }, + "cloud-dns": { + "slug": "cloud-dns", + "name": "Cloud DNS", + "four_words": "Programmable DNS serving", + "doc_url": "https://cloud.google.com/dns/docs/" + }, + "cloud-monitoring": { + "slug": "cloud-monitoring", + "name": "Cloud Monitoring", + "four_words": "Infrastructure and application monitoring", + "doc_url": "https://cloud.google.com/monitoring/docs/" + }, + "cloud-asset-inventory": { + "slug": "cloud-asset-inventory", + "name": "Cloud Asset Inventory", + "four_words": "All assets, one place", + "doc_url": "https://cloud.google.com/asset-inventory/docs/overview" + }, + "cloud-kms": { + "slug": "cloud-kms", + "name": "Cloud KMS", + "four_words": "Hosted key management service", + "doc_url": "https://cloud.google.com/kms/docs/" + }, + "places-library-maps-js-api": { + "slug": "places-library-maps-js-api", + "name": "Places Library, Maps JS API", + "four_words": "Places features for web", + "doc_url": "https://developers.google.com/maps/documentation/javascript/places" + }, + "cloud-tpu": { + "slug": "cloud-tpu", + "name": "Cloud TPU", + "four_words": "Hardware acceleration for ML", + "doc_url": "https://cloud.google.com/tpu/docs/" + }, + "geolocation-api": { + "slug": "geolocation-api", + "name": "Geolocation API", + "four_words": "Derive location without GPS", + "doc_url": "https://developers.google.com/maps/documentation/geolocation/overview" + }, + "cloud-functions-for-firebase": { + "slug": "cloud-functions-for-firebase", + "name": "Cloud Functions for Firebase", + "four_words": "Event-driven serverless applications", + "doc_url": "https://firebase.google.com/docs/functions" + }, + "vertex-ai-edge-manager": { + "slug": "vertex-ai-edge-manager", + "name": "Vertex AI Edge Manager", + "four_words": "Deploy monitor edge inferences", + "doc_url": "https://cloud.google.com/vertex-ai" + }, + "cloud-tools-for-visual-studio": { + "slug": "cloud-tools-for-visual-studio", + "name": "Cloud Tools for Visual Studio", + "four_words": "Visual Studio Google Cloud tools", + "doc_url": "https://cloud.google.com/tools/visual-studio/docs" + }, + "cloud-vpn": { + "slug": "cloud-vpn", + "name": "Cloud VPN", + "four_words": "Virtual private network connection", + "doc_url": "https://cloud.google.com/network-connectivity/docs/vpn/concepts/overview" + }, + "cloud-billing-api": { + "slug": "cloud-billing-api", + "name": "Cloud Billing API", + "four_words": "Programmatically manage Google Cloud billing", + "doc_url": "https://cloud.google.com/billing/docs/" + }, + "direct-peering": { + "slug": "direct-peering", + "name": "Direct Peering", + "four_words": "Peer with Google Cloud", + "doc_url": "https://cloud.google.com/network-connectivity/docs/direct-peering" + }, + "amp-for-email": { + "slug": "amp-for-email", + "name": "AMP for Email", + "four_words": "Dynamic interactive email", + "doc_url": "https://developers.google.com/apps-script/add-ons/gmail" + }, + "cloud-search": { + "slug": "cloud-search", + "name": "Cloud Search", + "four_words": "Unified search for enterprise", + "doc_url": "https://developers.google.com/cloud-search/docs/guides/" + }, + "operations-suite": { + "slug": "operations-suite", + "name": "Operations suite", + "four_words": "Monitoring, logging, troubleshooting", + "doc_url": "https://cloud.google.com/stackdriver/docs" + }, + "confidential-computing": { + "slug": "confidential-computing", + "name": "Confidential Computing", + "four_words": "Encrypt data in-use", + "doc_url": "https://cloud.google.com/compute/confidential-vm/docs" + }, + "talent-solutions": { + "slug": "talent-solutions", + "name": "Talent Solutions", + "four_words": "Job search with ML", + "doc_url": "https://cloud.google.com/talent-solution/docs" + }, + "classroom-api": { + "slug": "classroom-api", + "name": "Classroom API", + "four_words": "Provision and manage classrooms", + "doc_url": "https://developers.google.com/classroom/guides/get-started" + }, + "event-threat-detection": { + "slug": "event-threat-detection", + "name": "Event Threat Detection", + "four_words": "Scans for suspicious activity", + "doc_url": "https://cloud.google.com/security-command-center/docs/how-to-use-event-threat-detection" + }, + "cloud-build": { + "slug": "cloud-build", + "name": "Cloud Build", + "four_words": "DevOps Automation Platform", + "doc_url": "https://cloud.google.com/build/docs" + }, + "vertex-ai-tensorboard": { + "slug": "vertex-ai-tensorboard", + "name": "Vertex AI Tensorboard", + "four_words": "Managed TensorBoard for ML-experiment Visualization", + "doc_url": "https://cloud.google.com/vertex-ai/docs/experiments" + }, + "text-to-speech": { + "slug": "text-to-speech", + "name": "Text-To-Speech", + "four_words": "Convert text to audio", + "doc_url": "https://cloud.google.com/text-to-speech/docs/" + }, + "bare-metal-solution": { + "slug": "bare-metal-solution", + "name": "Bare Metal Solution", + "four_words": "Hardware for specialized workloads", + "doc_url": "https://cloud.google.com/bare-metal/docs/bms-planning" + }, + "data-studio": { + "slug": "data-studio", + "name": "Data Studio", + "four_words": "Collaborative data exploration / dashboarding", + "doc_url": "https://cloud.google.com/bigquery/docs/visualize-data-studio" + }, + "local-ssd": { + "slug": "local-ssd", + "name": "Local SSD", + "four_words": "VM locally attached SSDs", + "doc_url": "https://cloud.google.com/compute/docs/disks/local-ssd" + }, + "docs-api": { + "slug": "docs-api", + "name": "Docs API", + "four_words": "Create and edit documents", + "doc_url": "https://developers.google.com/docs/api/how-tos/overview" + }, + "cloud-profiler": { + "slug": "cloud-profiler", + "name": "Cloud Profiler", + "four_words": "CPU and heap profiling", + "doc_url": "https://cloud.google.com/profiler/docs/" + }, + "security-command-center": { + "slug": "security-command-center", + "name": "Security Command Center", + "four_words": "Security management and data risk platform", + "doc_url": "https://cloud.google.com/security-command-center/docs/" + }, + "deep-learning-containers": { + "slug": "deep-learning-containers", + "name": "Deep Learning Containers", + "four_words": "Preconfigured containers for deep learning", + "doc_url": "https://cloud.google.com/deep-learning-containers/docs" + }, + "cloud-domains": { + "slug": "cloud-domains", + "name": "Cloud Domains", + "four_words": "Register, transfer, manager domains", + "doc_url": "https://cloud.google.com/domains/docs" + }, + "apps-script": { + "slug": "apps-script", + "name": "Apps Script", + "four_words": "Extend and automate everything", + "doc_url": "https://developers.google.com/apps-script/" + }, + "cloud-logging": { + "slug": "cloud-logging", + "name": "Cloud Logging", + "four_words": "Centralized logging", + "doc_url": "https://cloud.google.com/logging/docs/" + }, + "apigee-api-platform": { + "slug": "apigee-api-platform", + "name": "Apigee API Platform", + "four_words": "Develop, secure, monitor APIs", + "doc_url": "https://cloud.google.com/apigee/docs" + }, + "looker": { + "slug": "looker", + "name": "Looker", + "four_words": "Enterprise BI and Analytics", + "doc_url": "https://cloud.google.com/looker" + }, + "network-service-tiers": { + "slug": "network-service-tiers", + "name": "Network Service Tiers", + "four_words": "Price vs performance tiering", + "doc_url": "https://cloud.google.com/network-tiers/docs/" + }, + "container-registry": { + "slug": "container-registry", + "name": "Container Registry", + "four_words": "Private container registry / storage", + "doc_url": "https://cloud.google.com/container-registry/docs/" + }, + "bigquery": { + "slug": "bigquery", + "name": "BigQuery", + "four_words": "Data warehouse / analytics", + "doc_url": "https://cloud.google.com/bigquery/docs/" + }, + "data-catalog": { + "slug": "data-catalog", + "name": "Data Catalog", + "four_words": "Metadata management service", + "doc_url": "https://cloud.google.com/data-catalog/docs/" + }, + "titan-security-key": { + "slug": "titan-security-key", + "name": "Titan Security Key", + "four_words": "Two-factor authentication (2FA) device", + "doc_url": "https://cloud.google.com/titan-security-key/#section-3" + }, + "firebase-authentication": { + "slug": "firebase-authentication", + "name": "Firebase Authentication", + "four_words": "Drop-in authentication", + "doc_url": "https://firebase.google.com/docs/auth" + }, + "cloud-healthcare-api": { + "slug": "cloud-healthcare-api", + "name": "Cloud Healthcare API", + "four_words": "Healthcare system GCP interoperability", + "doc_url": "https://cloud.google.com/healthcare-api/docs" + }, + "dialogflow": { + "slug": "dialogflow", + "name": "Dialogflow", + "four_words": "Create conversational interfaces", + "doc_url": "https://cloud.google.com/dialogflow/docs" + }, + "cloud-router": { + "slug": "cloud-router", + "name": "Cloud Router", + "four_words": "VPC / on-prem network route exchange (BGP)", + "doc_url": "https://cloud.google.com/network-connectivity/docs/router" + }, + "vertex-ai-matching-engine": { + "slug": "vertex-ai-matching-engine", + "name": "Vertex AI Matching Engine", + "four_words": "Vector similarity searches", + "doc_url": "https://cloud.google.com/vertex-ai/docs/matching-engine" + }, + "task-api": { + "slug": "task-api", + "name": "Task API", + "four_words": "Search, read & update Tasks", + "doc_url": "https://developers.google.com/tasks/reference/rest" + } +} diff --git a/other/train-to-cloud-city/devices/rfid/package-lock.json b/other/train-to-cloud-city/devices/rfid/package-lock.json index 00a98095..1274f696 100644 --- a/other/train-to-cloud-city/devices/rfid/package-lock.json +++ b/other/train-to-cloud-city/devices/rfid/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@google-cloud/pubsub": "^4.3.3", "dotenv": "^16.4.4", "express": "^4.18.3", "firebase": "^10.8.0", @@ -630,6 +631,68 @@ "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.10.5.tgz", "integrity": "sha512-eSkJsnhBWv5kCTSU1tSUVl9mpFu+5NXXunZc83le8GMjMlsWwQArSc7cJJ4yl+aDFY0NGLi0AjZWMn1axOrkRg==" }, + "node_modules/@google-cloud/paginator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.0.tgz", + "integrity": "sha512-87aeg6QQcEPxGCOthnpUjvw4xAZ57G7pL8FS0C4e/81fr3FjkpUpibf1s2v5XGyGhUVGF4Jfg7yEcxqn2iUw1w==", + "dependencies": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/precise-date": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/precise-date/-/precise-date-4.0.0.tgz", + "integrity": "sha512-1TUx3KdaU3cN7nfCdNf+UVqA/PSX29Cjcox3fZZBtINlRrXVTmUkQnCKv2MbBUbCopbK4olAT1IHl76uZyCiVA==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/projectify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz", + "integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/promisify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.0.0.tgz", + "integrity": "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/pubsub": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@google-cloud/pubsub/-/pubsub-4.3.3.tgz", + "integrity": "sha512-vJKh9L4dHf1XGSDKS1SB0IpqP/sUajQh4/QwhYasuq/NjzfHSxqSt+CuhrFGb5/gioTWE4gce0sn7h1SW7qESg==", + "dependencies": { + "@google-cloud/paginator": "^5.0.0", + "@google-cloud/precise-date": "^4.0.0", + "@google-cloud/projectify": "^4.0.0", + "@google-cloud/promisify": "^4.0.0", + "@opentelemetry/api": "^1.6.0", + "@opentelemetry/semantic-conventions": "~1.21.0", + "@types/duplexify": "^3.6.0", + "@types/long": "^4.0.0", + "arrify": "^2.0.0", + "extend": "^3.0.2", + "google-auth-library": "^9.3.0", + "google-gax": "^4.3.1", + "heap-js": "^2.2.0", + "is-stream-ended": "^0.1.4", + "lodash.snakecase": "^4.1.1", + "p-defer": "^3.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@grpc/grpc-js": { "version": "1.9.14", "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.14.tgz", @@ -788,6 +851,15 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, "node_modules/@mapbox/node-pre-gyp": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", @@ -896,6 +968,22 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/@opentelemetry/api": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.8.0.tgz", + "integrity": "sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.21.0.tgz", + "integrity": "sha512-lkC8kZYntxVKr7b8xmjCVUgE0a8xgDakPyDo9uSWavXPyYqLgYYGdEd2j8NxihRyb6UwpX3G/hFUF4/9q2V+/g==", + "engines": { + "node": ">=14" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -1132,6 +1220,32 @@ "url": "https://opencollective.com/serialport/donate" } }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/caseless": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", + "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==" + }, + "node_modules/@types/duplexify": { + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/@types/duplexify/-/duplexify-3.6.4.tgz", + "integrity": "sha512-2eahVPsd+dy3CL6FugAzJcxoraWhUghZGEQJns1kTKfCXWKJ5iG/VkaB05wRVrDKHfOFKqb0X0kXh91eE99RZg==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" + }, "node_modules/@types/node": { "version": "20.11.24", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz", @@ -1140,6 +1254,22 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/request": { + "version": "2.48.12", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.12.tgz", + "integrity": "sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==", + "dependencies": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + } + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==" + }, "node_modules/@types/w3c-web-usb": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/@types/w3c-web-usb/-/w3c-web-usb-1.0.10.tgz", @@ -1158,6 +1288,17 @@ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "optional": true }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -1195,7 +1336,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "optional": true, "dependencies": { "debug": "4" }, @@ -1284,12 +1424,52 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "engines": { + "node": ">=8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "devOptional": true }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "engines": { + "node": "*" + } + }, "node_modules/body-parser": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", @@ -1347,6 +1527,11 @@ "concat-map": "0.0.1" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1523,6 +1708,17 @@ "color-support": "bin.js" } }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/compare-versions": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-4.1.4.tgz", @@ -1623,6 +1819,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -1678,12 +1882,31 @@ "url": "https://dotenvx.com" } }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "optional": true }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -1711,6 +1934,14 @@ "iconv-lite": "^0.6.2" } }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -1920,6 +2151,14 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, "node_modules/exponential-backoff": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", @@ -1980,6 +2219,11 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2157,6 +2401,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -2219,6 +2476,55 @@ "node": ">=10" } }, + "node_modules/gaxios": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.3.0.tgz", + "integrity": "sha512-p+ggrQw3fBwH2F5N/PAI4k/G/y1art5OxKpb2J2chwNNHM4hHuAOtivjPuirMF4KNKwTTUal/lPfL2+7h2mEcg==", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gaxios/node_modules/agent-base": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", + "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/gaxios/node_modules/https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/gcp-metadata": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz", + "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==", + "dependencies": { + "gaxios": "^6.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -2292,6 +2598,56 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/google-auth-library": { + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.7.0.tgz", + "integrity": "sha512-I/AvzBiUXDzLOy4iIZ2W+Zq33W4lcukQv1nl7C8WUA6SQwyQwUwu3waNmWNAvzds//FG8SZ+DnKnW/2k6mQS8A==", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-gax": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.3.2.tgz", + "integrity": "sha512-2mw7qgei2LPdtGrmd1zvxQviOcduTnsvAWYzCxhOWXK4IQKmQztHnDQwD0ApB690fBQJemFKSU7DnceAy3RLzw==", + "dependencies": { + "@grpc/grpc-js": "~1.10.0", + "@grpc/proto-loader": "^0.7.0", + "@types/long": "^4.0.0", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "google-auth-library": "^9.3.0", + "node-fetch": "^2.6.1", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^2.0.0", + "protobufjs": "7.2.6", + "retry-request": "^7.0.0", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-gax/node_modules/@grpc/grpc-js": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.10.4.tgz", + "integrity": "sha512-MqBisuxTkYvPFnEiu+dag3xG/NBUDzSbAFAWlzfkGnQkjVZ6by3h4atbBc+Ikqup1z5BfB4BN18gKWR1YyppNw==", + "dependencies": { + "@grpc/proto-loader": "^0.7.10", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -2315,6 +2671,18 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2374,6 +2742,14 @@ "node": ">= 0.4" } }, + "node_modules/heap-js": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/heap-js/-/heap-js-2.5.0.tgz", + "integrity": "sha512-kUGoI3p7u6B41z/dp33G6OaL7J4DRqRYwVmeIlwLClx7yaaAy7hoDExnuejTKtuDwfcatGmddHDEOjf6EyIxtQ==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/http-cache-semantics": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", @@ -2429,7 +2805,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "optional": true, "dependencies": { "agent-base": "6", "debug": "4" @@ -2577,6 +2952,22 @@ "node": ">=8" } }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-stream-ended": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz", + "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2619,6 +3010,14 @@ "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", "optional": true }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -2637,6 +3036,25 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -2685,6 +3103,11 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==" + }, "node_modules/long": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", @@ -2987,7 +3410,6 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "optional": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -3187,6 +3609,14 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", @@ -3210,7 +3640,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "devOptional": true, "dependencies": { "wrappy": "1" } @@ -3232,6 +3661,14 @@ "node": ">= 0.8.0" } }, + "node_modules/p-defer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-3.0.0.tgz", + "integrity": "sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw==", + "engines": { + "node": ">=8" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -3376,6 +3813,17 @@ "node": ">=10" } }, + "node_modules/proto3-json-serializer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.1.tgz", + "integrity": "sha512-8awBvjO+FwkMd6gNoGFZyqkHZXCFd54CIYTb6De7dPaufGJ2XNW+QUNqbMr8MaAocMdb+KpsD4rxEOaTBDCffA==", + "dependencies": { + "protobufjs": "^7.2.5" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/protobufjs": { "version": "7.2.6", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.6.tgz", @@ -3491,7 +3939,6 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "optional": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -3527,6 +3974,19 @@ "node": ">= 4" } }, + "node_modules/retry-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz", + "integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==", + "dependencies": { + "@types/request": "^2.48.8", + "extend": "^3.0.2", + "teeny-request": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -3855,11 +4315,23 @@ "node": ">= 0.8" } }, + "node_modules/stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==" + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "optional": true, "dependencies": { "safe-buffer": "~5.2.0" } @@ -3928,6 +4400,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -3990,6 +4467,34 @@ "node": ">=8" } }, + "node_modules/teeny-request": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz", + "integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==", + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.9", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/teeny-request/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -4007,8 +4512,7 @@ "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "optional": true + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, "node_modules/tslib": { "version": "2.6.2", @@ -4126,8 +4630,7 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "optional": true + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/utils-merge": { "version": "1.0.1", @@ -4137,6 +4640,18 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -4148,8 +4663,7 @@ "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "optional": true + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "node_modules/websocket-driver": { "version": "0.7.4", @@ -4176,7 +4690,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "optional": true, "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -4243,8 +4756,7 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "devOptional": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/write-file-atomic": { "version": "5.0.1", diff --git a/other/train-to-cloud-city/devices/rfid/package.json b/other/train-to-cloud-city/devices/rfid/package.json index 6096569a..aefdcaec 100644 --- a/other/train-to-cloud-city/devices/rfid/package.json +++ b/other/train-to-cloud-city/devices/rfid/package.json @@ -10,6 +10,7 @@ "author": "", "license": "ISC", "dependencies": { + "@google-cloud/pubsub": "^4.3.3", "dotenv": "^16.4.4", "express": "^4.18.3", "firebase": "^10.8.0", diff --git a/other/train-to-cloud-city/devices/rfid/public/assets/reset.svg b/other/train-to-cloud-city/devices/rfid/public/assets/reset.svg new file mode 100644 index 00000000..212190b2 --- /dev/null +++ b/other/train-to-cloud-city/devices/rfid/public/assets/reset.svg @@ -0,0 +1 @@ + diff --git a/other/train-to-cloud-city/devices/rfid/public/assets/signal-green.svg b/other/train-to-cloud-city/devices/rfid/public/assets/signal-green.svg new file mode 100644 index 00000000..9a22a6db --- /dev/null +++ b/other/train-to-cloud-city/devices/rfid/public/assets/signal-green.svg @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/other/train-to-cloud-city/devices/rfid/public/assets/signal-red.svg b/other/train-to-cloud-city/devices/rfid/public/assets/signal-red.svg new file mode 100644 index 00000000..6357408a --- /dev/null +++ b/other/train-to-cloud-city/devices/rfid/public/assets/signal-red.svg @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/other/train-to-cloud-city/devices/rfid/public/assets/signal-yellow.svg b/other/train-to-cloud-city/devices/rfid/public/assets/signal-yellow.svg new file mode 100644 index 00000000..e4b8ac9f --- /dev/null +++ b/other/train-to-cloud-city/devices/rfid/public/assets/signal-yellow.svg @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/other/train-to-cloud-city/devices/rfid/public/assets/start.svg b/other/train-to-cloud-city/devices/rfid/public/assets/start.svg new file mode 100644 index 00000000..8c0c2b9f --- /dev/null +++ b/other/train-to-cloud-city/devices/rfid/public/assets/start.svg @@ -0,0 +1 @@ + diff --git a/other/train-to-cloud-city/devices/rfid/public/assets/stop.svg b/other/train-to-cloud-city/devices/rfid/public/assets/stop.svg new file mode 100644 index 00000000..8807bc7b --- /dev/null +++ b/other/train-to-cloud-city/devices/rfid/public/assets/stop.svg @@ -0,0 +1 @@ + diff --git a/other/train-to-cloud-city/devices/rfid/public/assets/top-banner.svg b/other/train-to-cloud-city/devices/rfid/public/assets/top-banner.svg new file mode 100644 index 00000000..82c14724 --- /dev/null +++ b/other/train-to-cloud-city/devices/rfid/public/assets/top-banner.svg @@ -0,0 +1,814 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/other/train-to-cloud-city/devices/rfid/public/index.css b/other/train-to-cloud-city/devices/rfid/public/index.css index 07c502c7..6ca06ab4 100644 --- a/other/train-to-cloud-city/devices/rfid/public/index.css +++ b/other/train-to-cloud-city/devices/rfid/public/index.css @@ -1,10 +1,116 @@ +:root { + --font: "Pixelify Sans"; + --melanzane: #211e20; + --smoky: #555568; + --lemonGrass: #a0a08b; + --lilyWhite: #e9efec; + --darkOrange: #ff8100; + --cinnabar: #e94434; + --dodgerblue: #4286f5; + --selectiveyellow: #fbbb06; + --eucalyptus: #34a752; +} + body { - font-family: "monospace"; + font-family: var(--font); + font-size: larger; + background-color: var(--lilyWhite); + margin: auto 0; +} + +.button { + display: flex; + align-items: center; + padding: 10px; + margin: 5px; + background: var(--selectiveyellow); + border: 2px solid var(--melanzane); + border-bottom-left-radius: 10px; + border-bottom-right-radius: 10px; + border-top-left-radius: 10px; + border-top-right-radius: 10px; +} + +.button a { + text-decoration: none; + color: var(--melanzane); + font-family: var(--font); + font-size: larger; +} + +.button img { + width: 20px; + height: auto; + margin-right: 15px; +} + +.button.start { + background-color: var(--eucalyptus); +} + +.button.reset { + background-color: var(--dodgerblue); +} + +.button.stop { + background-color: var(--cinnabar); } -.adminPanel { +.headerBanner { display: flex; + align-items: center; + justify-content: center; + min-height: 155px; + background-image: url(./assets/top-banner.svg); + background-size: cover; + background-repeat: no-repeat; + background-position: center; +} + +.retroBorder .blue { + height: 3px; + background: var(--dodgerblue); +} + +.retroBorder .red { + height: 3px; + background: var(--cinnabar); +} + +.retroBorder .yellow { + height: 3px; + background: var(--selectiveyellow); +} + +.retroBorder .green { + height: 3px; + background: var(--eucalyptus); +} + +.container { + display: flex; + align-items: center; + justify-content: center; flex-direction: column; - justify-content: space-evenly; - margin: 10px auto; + margin: 50px; +} + +.section { + border: 1px solid var(--melanzane); + padding: 10px; + border-bottom-right-radius: 10px; + border-bottom-left-radius: 10px; + border-top-right-radius: 10px; + border-top-left-radius: 10px; + background-color: white; + margin: 10px; + width: 100%; + max-width: 500px; +} + +.signals { + display: flex; + align-items: center; + justify-content: center; + margin-top: 20px; } diff --git a/other/train-to-cloud-city/devices/rfid/public/index.html b/other/train-to-cloud-city/devices/rfid/public/index.html index cd3f88b3..5d4b3563 100644 --- a/other/train-to-cloud-city/devices/rfid/public/index.html +++ b/other/train-to-cloud-city/devices/rfid/public/index.html @@ -1,24 +1,54 @@ - Cloud Train Admin - - - - + + + + + + +
-

Cloud Train Admin

+
+

Cloud Train Admin

+
+
+
+
+
+
+
+
+
+ signal-green + signal-red + signal-yellow +
+
+
+
+
+
+
+
diff --git a/other/train-to-cloud-city/devices/rfid/start.js b/other/train-to-cloud-city/devices/rfid/start.js index 64b9e2a1..e46bd953 100644 --- a/other/train-to-cloud-city/devices/rfid/start.js +++ b/other/train-to-cloud-city/devices/rfid/start.js @@ -17,76 +17,106 @@ const express = require("express"); const path = require("path"); const url = require("url"); -const { initTrain } = require("./utils/train.js"); -const { ports, parsers } = require("./utils/checkpoints.js"); -const { poweredUP } = require("./utils/firebase.js"); +const { initTrain, getMotor } = require("./utils/train.js"); +const { getPorts } = require("./utils/checkpoints.js"); const { - readTrain, - updateTrainMovement, -} = require("./utils/trainState.js"); + setMissionPattern, + updateLocation, + updateInputMailbox, +} = require("./utils/firestoreHelpers.js"); +const { readCargo, resetGameState, updateGameLoop } = require("./trainGame.js"); const expressApp = express(); expressApp.use(express.json()); expressApp.use(express.static("express")); expressApp.use(express.static("public")); +const { SerialPort, ReadlineParser } = require("serialport"); + /** - * listenToReaders + * listenToReaders (setup) * ---------------------- * -> should update state of current train location * -> push up information to firestore */ -function listenToReaders() { - parsers[0].on("data", (chunk) => readTrain(chunk, 0)); - parsers[1].on("data", (chunk) => readTrain(chunk, 1)); - parsers[2].on("data", (chunk) => readTrain(chunk, 2)); - parsers[3].on("data", (chunk) => readTrain(chunk, 3)); +async function listenToReaders() { + const ports = await getPorts(); + + ports?.forEach((port, index) => { + const listener = new SerialPort(port).pipe(new ReadlineParser()); + // listeners are passed their location role (i.e station, checkpoint, etc); + switch (port?.role) { + case "mission_check": { + listener.on("data", (chunk) => setMissionPattern(chunk, port?.role)); + break; + } + case "station": { + listener.on("data", (chunk) => readCargo(chunk, port?.role)); + break; + } + default: { + listener.on("data", () => updateLocation(port?.role)); + } + } + }); } /** * initialize */ -(function initialize(useStubTrain = false) { - !useStubTrain && initTrain(); +(async function initialize() { + initTrain(); listenToReaders(); + await updateInputMailbox("reset"); })(); /** * GET /stop */ -expressApp.get("/stop", (req, res) => { +expressApp.get("/stop", async (req, res) => { console.log("Stopping train ..."); - updateTrainMovement(false); - res.redirect(`/?message=${encodeURIComponent("Stopping train")}`); + + try { + const motor = await getMotor(); + motor.stop(); + } catch(error) { + res.send(`/?message=${encodeURIComponent("Stopping train")}`); + res.status(400).json({error}); + } }); /** * get /reset */ -expressApp.get("/reset", (req, res) => { +expressApp.get("/reset", async (req, res) => { console.log("Resetting train state ..."); - // TODO: Move train to station - res.redirect(`/?message=${encodeURIComponent("Resetting train")}`); + + try { + resetGameState(); + await updateInputMailbox("reset"); + res.status(200).redirect(`/?message=${encodeURIComponent("Resetting train")}`); + } catch(error) { + console.error(error); + res.status(400).json({error}) + } }); /** * GET /start */ -expressApp.get("/start", (req, res) => { +expressApp.get("/start", async (req, res) => { const urlParts = url.parse(req.url, true); const query = urlParts.query; - const useStubTrain = query["train"] === "dummy"; - // Start train movment - updateTrainMovement(true, 30); - - useStubTrain - ? console.log("Starting dummy train ...") - : console.log("Starting train ..."); - - res.redirect( - `/?message=${encodeURIComponent(useStubTrain ? "Starting dummy train" : "Starting train")}`, - ); + try { + const motor = await getMotor(); + motor.setPower(30); + console.log("Starting train demo ..."); + res.status(200).redirect(`/?message=${encodeURIComponent("Starting train")}`); + } catch (error) { + console.error(error); + res.status(400).json({error}) + } }); /** @@ -99,4 +129,20 @@ expressApp.get("/", (req, res) => { res.sendFile(path.join(__dirname + "/public/index.html")); }); +async function gracefulExit() { + console.log("Caught interrupt signal. Resetting the game."); + try { + await updateInputMailbox("reset"); + console.log("Reset completed, exiting out."); + } catch (error) { + console.log(error); + } + process.exit(0); +} + +// Attempt graceful exit if ctrl-c or unexpected crash +[`exit`, `SIGINT`, `uncaughtException`, `SIGTERM`].forEach((eventType) => + process.on(eventType, gracefulExit) +) + expressApp.listen(3000, () => console.log("Listening to 3000")); diff --git a/other/train-to-cloud-city/devices/rfid/trainGame.js b/other/train-to-cloud-city/devices/rfid/trainGame.js new file mode 100644 index 00000000..8e003fdf --- /dev/null +++ b/other/train-to-cloud-city/devices/rfid/trainGame.js @@ -0,0 +1,206 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const { StringDecoder } = require("node:string_decoder"); +const { + publishMessage, + getTrain, + getTrainMailbox, + getSessionMailbox, + getProposalResult, + clearTrainMailbox, + trainMailboxListener, + proposalListener, + updateInputMailbox, + validateCargo, + updateLocation, + submitActualCargo, +} = require("./utils/firestoreHelpers.js"); +const { getMotor } = require("./utils/train.js"); + +// Cargo reading +let beginReading = false; +let stockedCargo = []; + +// Cargo results & Mailbox +let trainMailbox = {}; +let proposalResult = {}; + +// Train movement states +let moveBackToStation = false; +let moveForwardsToStation = false; + +/** + * Train mailbox listener + */ +trainMailboxListener(async (snapshot) => { + trainMailbox = snapshot?.data(); + trainMailbox && (await updateGameLoop()); +}); + +/** + * Proposal listener + */ +proposalListener(async (snapshot) => { + proposalResult = snapshot?.data()?.proposal_result; + + if (proposalResult) { + const motor = await getMotor(); + + await publishMessage("cargo-result", { + trainMailbox, + proposalResult, + timestamp: Date.now(), + }); + + if (trainMailbox?.input === "do_check_cargo") { + // If cargo isn't valid + // Head back to station to reload cargo + if (proposalResult?.reason && !proposalResult?.clear) { + moveBackToStation = true; + motor?.setPower(-30); + stockedCargo = []; + await publishMessage("cargo-reload", { + trainMailbox, + proposalResult, + timestamp: Date.now(), + }); + } + } + if (trainMailbox?.input === "do_victory_lap") { + await publishMessage("victory", { + sessionComplete: true, + trainMailbox, + proposalResult, + timestamp: Date.now(), + }); + } + } +}); + +/** + * readCargo + * ---------------------- + */ +async function readCargo(chunk, role) { + const motor = await getMotor(); + + // In either cargo error & reload stage (backwards to station) + // Or in victory lap mode (forwards to station) + if (moveBackToStation || moveForwardsToStation) { + await moveToStation(chunk, role); + return; + } + + const tagId = new String(chunk); + const frontCar = "\x03\x02330035AD1EB5\r"; + const backCar = "\x03\x023300348E9019\r"; + + const isFrontCar = frontCar.includes(tagId); + const isBackCar = backCar.includes(tagId); + const isCargo = !isFrontCar && !isBackCar; + + // MIDDLE: In the middle of train, store cargo chunk and continue on + if (isCargo && beginReading) { + stockedCargo.push(chunk); + } + // FRONT: Begin reading cargo + if (isFrontCar) { + beginReading = true; + } + // BACK: At tailend of train, wrap up and send read cargo to firestore + if (isBackCar) { + beginReading = false; + motor?.brake(); + motor?.stop(); + + try { + // Submit held cargo + await submitActualCargo(stockedCargo); + } catch (error) { + console.error(error); + } + } +} + +/** + * moveToStation + * ---------------------- + * In either cargo error & reload stage (backwards to station) + * Or in victory lap mode (forwards to station) + */ +async function moveToStation(chunk, role) { + const tagId = new String(chunk); + const frontCar = "\x03\x02330035AD1EB5\r"; + const isFrontCar = frontCar.includes(tagId); + const motor = await getMotor(); + + if (isFrontCar && role === "station") { + motor?.brake(); + motor?.stop(); + moveBackToStation = false; + moveForwardsToStation = false; + return; + } + + moveForwardsToStation && motor?.setPower(30); + moveBackToStation && motor?.setPower(-40); +} + +/** + * updateGameLoop + * ---------------------- + * Main game loop for train. Callback fn to all serialport / rfid readers + * so state is not held within loop, but above (TODO: refactor later) + */ +async function updateGameLoop() { + const motor = await getMotor(); + motor?.brake(); + motor?.stop(); + + if (trainMailbox?.input === "do_check_cargo") { + motor?.setPower(30); + return; + } + + if (trainMailbox?.input === "do_victory_lap") { + if (moveForwardsToStation) { + moveForwardsToStation = false; // reset + motor?.stop(); + // Reset train mailbox + await clearTrainMailbox(); + // TODO: Send metrics + console.log("Session success!"); + await updateInputMailbox("reset"); + } else { + moveForwardsToStation = true; + motor?.setPower(40); + console.log("Going on victory lap!"); + } + } +} + +async function resetGameState() { + // Reset cargo reading + beginReading = false; + stockedCargo = []; + // Reset cargo results & Mailbox + trainMailbox = {}; + proposalResult = {}; + // Reset train movement states + moveBackToStation = false; + moveForwardsToStation = false; +} + +module.exports = { readCargo, updateGameLoop, resetGameState }; diff --git a/other/train-to-cloud-city/devices/rfid/utils/checkpoints.js b/other/train-to-cloud-city/devices/rfid/utils/checkpoints.js index ea4c352f..289c035a 100644 --- a/other/train-to-cloud-city/devices/rfid/utils/checkpoints.js +++ b/other/train-to-cloud-city/devices/rfid/utils/checkpoints.js @@ -12,34 +12,37 @@ // See the License for the specific language governing permissions and // limitations under the License. -const { SerialPort, ReadlineParser } = require("serialport"); +const { SerialPort } = require("serialport"); -// Train checkpoints -const ports = [ - new SerialPort({ - path: "/dev/ttyUSB0", - baudRate: 9600, - }), - new SerialPort({ - path: "/dev/ttyUSB1", - baudRate: 9600, - }), - new SerialPort({ - path: "/dev/ttyUSB2", - baudRate: 9600, - }), - new SerialPort({ - path: "/dev/ttyUSB3", - baudRate: 9600, - }), -]; +const roles = { + A10LXV9L: "mission_check", + A10LXV9Y: "station", + A10LXV95: "checkpoint_1", + A10LXVA5: "checkpoint_2", + A10LY36P: "checkpoint_3", + A10LY36T: "checkpoint_4", +}; -// Parsers -const parsers = [ - ports[0].pipe(new ReadlineParser()), - ports[1].pipe(new ReadlineParser()), - ports[2].pipe(new ReadlineParser()), - ports[3].pipe(new ReadlineParser()), -]; +// Train checkpoints +const getPorts = async function () { + let ports = []; + try { + const list = await SerialPort.list(); + list.forEach((port, index) => { + const role = roles[port.serialNumber]; + if (role) { + ports.push({ + role, + path: port.path, + serialNumber: port.serialNumber, + baudRate: 9600, + }); + } + }); + } catch (error) { + console.error(error); + } + return ports; +}; -module.exports = { ports, parsers }; +module.exports = { getPorts }; diff --git a/other/train-to-cloud-city/devices/rfid/utils/firestoreHelpers.js b/other/train-to-cloud-city/devices/rfid/utils/firestoreHelpers.js new file mode 100644 index 00000000..72440fcf --- /dev/null +++ b/other/train-to-cloud-city/devices/rfid/utils/firestoreHelpers.js @@ -0,0 +1,291 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const { firebase, db, app, firestore } = require("./firebase.js"); +const { StringDecoder } = require("node:string_decoder"); +const { getMotor } = require("./train.js"); +const { PubSub } = require("@google-cloud/pubsub"); + +const pubSubClient = new PubSub(); + +async function publishMessage(topic, data) { + const dataBuffer = Buffer.from(JSON.stringify(data)); + try { + const messageId = await pubSubClient.topic(topic).publishMessage({ data: dataBuffer }); + console.log(`Message ${messageId} published.`); + } catch (error) { + console.error(`Received error while publishing: ${error.message}`); + process.exitCode = 1; + } +} + +/** + * setMissionPattern + * ---------------------- + */ +async function setMissionPattern(chunk, reader) { + const motor = await getMotor(); + const matchingTag = await getMatchingTag({ chunk }); + const proposalRef = db.collection("global").doc("proposal"); + const mailboxRef = db.collection("global").doc("input_mailbox"); + + /** + * PATTERNS TO SELECT FROM + * high complex - 3300348D69E3 + * medium complex - 33003558732D + * low complex - 0D0088F32B5D + */ + if (matchingTag?.pattern_slug) { + try { + await proposalRef.set( + { pattern_slug: matchingTag?.pattern_slug }, + { merge: false }, + ); + + await publishMessage("mission-selected", { + chunk, + timestamp: Date.now(), + pattern_slug: matchingTag?.pattern_slug, + }); + + console.log( + `Mission has been read: ${JSON.stringify(matchingTag?.pattern_slug)}. Waiting for event input trigger.`, + ); + } catch (error) { + console.error(error); + } + return; + } + + /** + * EVENT RFID TAG (let's go magic wand) + * check-pattern (GO) - 0D00876E4DA9 + * stop 330034B6B607 + * reset - 330035D3B96C + */ + if (matchingTag?.event_slug) { + const { event_slug } = matchingTag; + + if (event_slug === "check-pattern") { + console.log("Checking pattern ...."); + try { + await mailboxRef.set({ input: "check_pattern" }); + motor?.setPower(30); // move towards station + console.log( + `Input mailbox has been triggered to check pattern and now moving to station.`, + ); + } catch (error) { + motor?.stop(); + console.error(error); + } + } + + if (event_slug === "reset") { + console.log("Admin reset registered."); + try { + motor.stop(); + await mailboxRef.set({ input: "reset" }); + moveBackToStation = true; + motor?.setPower(30); + } catch (error) { + console.error(error); + } + } + + if (event_slug === "stop") { + console.log("Admin stop registered."); + motor?.stop(); + } + } +} + +/** + * getMatchingTag + * ---------------------- + * Fetches GCP services, mission patterns, event tags (i.e eval trigger tag) + */ +async function getMatchingTag(id) { + const tagRef = db.collection("tags"); + let tag = {}; + try { + const snapshot = await tagRef.get(); + snapshot.docs.forEach((doc) => { + const data = doc.data(); + const chunk = new String(id?.chunk); + const docId = new String(doc.id); + if (chunk.includes(docId)) { + tag = data; + } + }); + } catch (error) { + console.error(error); + } + return tag; +} + +/** + * getTrain + * ---------------------- + */ +async function getTrain() { + const trainRef = db.collection("global").doc("train"); + let doc = {}; + try { + const snapshot = await trainRef.get(); + doc = snapshot.data(); + } catch (error) { + console.error(error); + } + + return doc; +} + +/** + * getTrainMailbox + * ---------------------- + */ +async function getTrainMailbox() { + const trainRef = db.collection("global").doc("train_mailbox"); + let doc = {}; + + try { + const snapshot = await trainRef.get(); + doc = snapshot.data(); + } catch (error) { + console.error(error); + } + + return doc; +} + +/** + * clearTrainMailbox + * ---------------------- + */ +async function clearTrainMailbox() { + const ref = db.collection("global").doc("train_mailbox"); + try { + await ref.update({ input: null }, { merge: true }); + console.log(`Train mailbox cleared`); + } catch (error) { + console.error(error); + } +} + +/** + * matchCargoToServices + *------------------- + * chunks -> rfid tag id + */ +async function matchCargoToServices(chunks) { + let cargos = []; + + for (const chunk of chunks) { + const buffer = Buffer.from(JSON.stringify({ chunk })); + const id = JSON.parse(buffer.toString()); + // Match read rfid chunk with correct service + const matchingService = await getMatchingTag(id); + + if (matchingService?.slug) { + cargos.push(matchingService.slug); + } + } + + return cargos; +} + +/** + * submitActualCargo + *------------------- + * chunks -> rfid tag id + */ +async function submitActualCargo(chunks) { + let cargos = await matchCargoToServices(chunks); + // Updates actual_cargo state with newly read service + const ref = db.collection("global").doc("cargo"); + try { + await ref.update({ actual_cargo: cargos }, { merge: true }); + await publishMessage("cargo-read", { + actualCargo: cargos, + timestamp: Date.now(), + }); + + console.log(`Cargos read ${JSON.stringify(cargos)}`); + } catch (error) { + console.error(error); + } +} + +/** + * updateLocation + *------------------- + */ +async function updateLocation(location) { + const ref = db.collection("global").doc("train"); + try { + await ref.update({ actual_location: location }, { merge: true }); + await publishMessage("location-updated", { + location, + timestamp: Date.now(), + }); + console.log(`Passed checkpoint ${location}`); + } catch (error) { + console.error(error); + } +} + +/** + * updateInputMailbox + *------------------- + * index -> step train is on + */ +async function updateInputMailbox(eventString) { + const ref = db.collection("global").doc("input_mailbox"); + try { + await ref.update({ input: eventString }, { merge: true }); + } catch (error) { + console.error(error); + } +} + +/** + * trainMailboxListener + * ---------------------- + */ +async function trainMailboxListener(cb = () => {}) { + const trainRef = db.collection("global").doc("train_mailbox"); + trainRef.onSnapshot(cb); +} + +/** + * proposalListener + * ---------------------- + */ +async function proposalListener(cb = () => {}) { + const proposalRef = db.collection("global").doc("proposal"); + proposalRef.onSnapshot(cb); +} + +module.exports = { + publishMessage, + getTrain, + getTrainMailbox, + trainMailboxListener, + proposalListener, + setMissionPattern, + updateLocation, + updateInputMailbox, + submitActualCargo, + clearTrainMailbox, +}; diff --git a/other/train-to-cloud-city/devices/rfid/utils/train.js b/other/train-to-cloud-city/devices/rfid/utils/train.js index af02e3c2..76ca92f2 100644 --- a/other/train-to-cloud-city/devices/rfid/utils/train.js +++ b/other/train-to-cloud-city/devices/rfid/utils/train.js @@ -13,7 +13,6 @@ // limitations under the License. const { db, app, firestore } = require("./firebase.js"); -const { getTrainMovement } = require("./trainState.js"); const PoweredUP = require("node-poweredup"); const poweredUP = new PoweredUP.PoweredUP(); @@ -23,33 +22,22 @@ const poweredUP = new PoweredUP.PoweredUP(); function initTrain() { poweredUP?.on("discover", async (hub) => { console.log(`Discovered ${hub.name}!`); - hub.connect(); - - const motorA = await hub.waitForDeviceAtPort("A"); + hub?.connect(); console.log("Connected"); - - // Main controller of the train, - // checkpoints may manipulate the firestore train state - // which is constantly checked on a loop - while (true) { - const { isRunning, power } = await getTrainMovement(); - // TODO: Try swapping onSnapshot listener rather than while(true) - // to react only when the train collection is updated - - if (isRunning) { - motorA.setPower(power || 30); - } else { - console.log('STOPPPP'); - motorA.brake(); - motorA.setPower(0); - motorA.stop(); - await hub.sleep(3000); - } - } }); poweredUP.scan(); // Start scanning for Hubs - console.log("Scanning for Hubs..."); } -module.exports = { initTrain, poweredUP }; +async function getMotor() { + const hubs = poweredUP?.getHubs(); + let motor; + try { + motor = await hubs[0]?.waitForDeviceAtPort("A"); + } catch (error) { + console.log(error); + } + return motor; +} + +module.exports = { initTrain, poweredUP, getMotor }; diff --git a/other/train-to-cloud-city/devices/rfid/utils/trainState.js b/other/train-to-cloud-city/devices/rfid/utils/trainState.js deleted file mode 100644 index 8477b272..00000000 --- a/other/train-to-cloud-city/devices/rfid/utils/trainState.js +++ /dev/null @@ -1,258 +0,0 @@ -// Copyright 2024 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -const { LocalStorage } = require("node-localstorage"); -const { firebase, db, app, firestore } = require("./firebase.js"); -const { StringDecoder } = require("node:string_decoder"); -const decoder = new StringDecoder("utf8"); -const sleep = ms => new Promise(r => setTimeout(r, ms)); - -// TODO: Find if localstorage is best place to store -let beginReading = false; -let holdCargo = []; - - -/** - * readTrain - * ---------------------- - * TODO: To replace checkpointCrossed - */ -async function readTrain(chunk, checkpoint) { - // This could be separate before the game begins - //const patternSelected = rfid; (after backcar) - const tagId = new String(chunk); - // TODO: Replace with calls to firestore - const frontCar = '\x02330035AD1EB5\r'; - const backCar = '\x03\x023300348E9019\r'; - - const isFrontCar = frontCar.includes(tagId); - const isBackCar = backCar.includes(tagId); - const isCargo = !isFrontCar && !isBackCar; - - // MIDDLE: In the middle of train, store cargo chunk and continue on - if(isCargo && beginReading) { - holdCargo.push(chunk); - return; - } - - // FRONT: Begin reading cargo - if(isFrontCar) { - beginReading = true - try { - await updateLocation(chunk, checkpoint); - } catch(error) { - console.error(error); - } - } - - // BACK: At tailend of train, wrap up and send read cargo to firestore - if(isBackCar) { - beginReading = false - actual_cargo = holdCargo - holdCargo = []; // clear holding cargo - - // TODO: save actual_cargo to firestore - // TODO: pubsub metric to log beginning game (for event) - // Verification happens at the station (checkpoint 0) - if(checkpoint === 0) { - Promise.all([ - // updateCargo(holdCargo, 0), - // validateCargo(holdCargo, 0) - ]) - .catch((error) => { - console.error(error); - }); - // TODO: set signal lights - } - } -} - -/** - * getMatchingService - * ---------------------- - */ -async function getMatchingService(docId) { - const serviceRef = db.collection("tags"); - let services = []; - - try { - const snapshot = await serviceRef.get(); - snapshot.docs.forEach((doc) => { - const chunk = new String(docId.chunk); - const docId = new String(doc.id); - if (chunk.includes(docId)) { - services.push(doc.data()); - } - }); - } catch(error) { - console.error(error); - } - - return services; -} - -/** - * updateCargo - *------------------- - * chunk -> rfid tag id - * index -> step train is on - */ -async function updateCargo(chunk, index) { - const buffer = Buffer.from(JSON.stringify({ chunk })); - const docId = JSON.parse(buffer.toString()); - - // Match read rfid chunk with correct service - const matchingService = await getMatchingService(docId); - // Updates actual_cargo state with newly read service - const ref = db.collection("global").doc("world"); - - try { - const cargoSnapshot = await ref.get(); - const { train } = cargoSnapshot.data(); - - const newCargo = { - reader: index, - service: matchingService, - }; - const trainUpdate = { - train: { - ...train, - actual_cargo: [...train?.actual_cargo, newCargo], - }, - }; - await ref.update(trainUpdate, { merge: true }); - console.log(`Cargo read ${JSON.stringify(newCargo)}`); - } catch(error) { - console.error(error); - } -} - -/** - * updateLocation - *------------------- - * index -> step train is on - */ -async function updateLocation(chunk, index = 0) { - const ref = db.collection("global").doc("world"); - - try { - const snapshot = await ref.get(); - const { train } = snapshot.data(); - const trainUpdate = { - train: { - ...train, - actual_location: index, - }, - }; - - // Move train if train is not at location they need to be - const locationReached = index === train.target_location; - - await ref.update(trainUpdate, { merge: true }); - console.log(`Passed checkpoint ${index}`); - - if(locationReached) { - await updateTrainMovement(false, 0); - } - } catch(error) { - console.error(error); - } -} - -/** - * getTrainMovement - * -------------------------- - */ -async function getTrainMovement() { - const ref = db.collection("train"); - let movement = []; - - try { - const snapshot = await ref.get(); - snapshot.forEach((doc) => movement.push(doc.data())); - } catch(error) { - console.error(error); - } - - return movement?.[0]; -} - -async function moveTrainToCheckpoint(currentIndex, targetIndex) { - await updateTrainMovement(currentIndex !== targetIndex, 30); -} - -/** - * updateTrainMovement - * -------------------------- - * power: -100 (backwards) -> +100 (forward) - */ -async function updateTrainMovement(isRunning, power = 30) { - try { - const state = { isRunning, power }; - const ref = db.collection("train").doc("state"); - await ref.set(state, { merge: true }); - console.log(`Train is running: ${isRunning}`); - } catch(error) { - console.error(error); - } -} - -/** - * validateCargo - *------------------- - * cargos[] -> list of services - * index -> step train is on - */ -async function validateCargo(cargos) { - const ref = db.collection("global").doc("world"); - - try { - const snapshot = await ref.get(); - const { train } = snapshot.data(); - const nextStop = train.actual_location > 2 ? 0 : train.actual_location + 1; // checkpoints 0 -> 3 - // TODO: Add in here the code to connect with validation api to pass actual cargo - /** - * const response = await fetch(......) - * - * if (response.status === 200) { - * // update actual_cargo - * // update target_location to 0 ---> move all the way around the track - * } else { - * const { erroredCheckpoint } = response; - * // update target_location to erroredCheckpoint marker - * // train should move forward and then move back to station with red signal lights - * } - */ - const trainUpdate = { - train: { - ...train, - target_location: 3, - }, - }; - // Update new target location after successful validation - await ref.update(trainUpdate, { merge: true }); - console.log(`Target location for train is checkpoint ${nextStop}.`); - } catch(error) { - console.error(error); - } -} - -module.exports = { - readTrain, - validateCargo, - getTrainMovement, - updateLocation, - updateCargo, - updateTrainMovement, -}; diff --git a/other/train-to-cloud-city/docs/information_flow.md b/other/train-to-cloud-city/docs/information_flow.md index e35b5ba2..198ef5a4 100644 --- a/other/train-to-cloud-city/docs/information_flow.md +++ b/other/train-to-cloud-city/docs/information_flow.md @@ -1,30 +1,30 @@ - # Overview -At a high level, we have two independent information loops. +At a high level, we have two independent information loops. -* Validation of a proposal (pattern + proposal) -* World state: Train (including cargo), Signals, more later? +- Validation of a proposal (pattern + proposal) +- World state: Train (including cargo), Signals, more later? -To coordinate user input, train motion, validation and displaying overall state we have a central Reconciler. Inspired by Kubernetes reconciliation loops (compare desired state to actual state, make changes to move actual towards desired) and game servers. +To coordinate user input, train motion, validation and displaying overall state we have a central Reconciler. Inspired by Kubernetes reconciliation loops (compare desired state to actual state, make changes to move actual towards desired) and game servers. ![information flow diagram](information_flow_a.png) ## Pattern validation information flow -Logically, this is a single request/response flow. +Logically, this is a single request/response flow. (`Pattern.slug`, `Proposal`) --> `PatternResult` -It is a deterministic function, always returning the same result for the same input. +It is a deterministic function, always returning the same result for the same input. -The Reconciler may call this repeatedly over time if the train cargo changes. +The Reconciler may call this repeatedly over time if the train cargo changes. # Data Structures -### World +### World + +World state: -World state: ``` slug, name, @@ -35,7 +35,9 @@ World state: ``` ### GCP metadata + Service example: + ``` { "slug": "cloud-run", @@ -44,11 +46,13 @@ Service example: "doc_url": "https://cloud.google.com/run/docs" } ``` + ### Patterns & Proposals (TODO for all of these, link to actual definition in code/JSON once it exists) Checkpoint: + ``` slug, name, @@ -56,30 +60,34 @@ Checkpoint: satisfying_services: [Service.slug, ...], ``` -Pattern: +Pattern: + ``` slug, - name, + name, description, checkpoints: [Checkpoint, ...], ``` Proposal: + ``` pattern_slug, service_slugs: [Service.slug, ...], ``` CheckpointResult: + ``` checkpoint: Checkpoint, valid: bool, reason: string, ``` -ProposalResult: +ProposalResult: + ``` - valid: bool, + valid: bool, reason: string, proposal: Proposal, pattern: Pattern, diff --git a/other/train-to-cloud-city/package-lock.json b/other/train-to-cloud-city/package-lock.json new file mode 100644 index 00000000..90b7979d --- /dev/null +++ b/other/train-to-cloud-city/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "train-to-cloud-city", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/other/train-to-cloud-city/terraform/README.md b/other/train-to-cloud-city/terraform/README.md index 0beac348..4c66f7ff 100644 --- a/other/train-to-cloud-city/terraform/README.md +++ b/other/train-to-cloud-city/terraform/README.md @@ -2,25 +2,25 @@ ### Requirements -* `gcloud` cli (To install [click here](https://cloud.google.com/sdk/docs/install)) -* `terraform` cli (To install [click here](https://developer.hashicorp.com/terraform/tutorials/gcp-get-started/install-cli)) -* Google Cloud Platform project (To create a project [click here](https://cloud.google.com/resource-manager/docs/creating-managing-projects#gcloud)) +- `gcloud` cli (To install [click here](https://cloud.google.com/sdk/docs/install)) +- `terraform` cli (To install [click here](https://developer.hashicorp.com/terraform/tutorials/gcp-get-started/install-cli)) +- Google Cloud Platform project (To create a project [click here](https://cloud.google.com/resource-manager/docs/creating-managing-projects#gcloud)) ## Getting Started 1. Navigate to any of the following patterns directories: - * [Pattern A - CI/CD pipeline](./pattern_A/) - * [Pattern B - Provision Cloud Firestore with Cloud Run](./pattern_B/) - * [Pattern C - Compute Engine with Node.js](./pattern_C/) - * [Pattern D - Host a database backed website](./pattern_D/) + - [Pattern A - CI/CD pipeline](./pattern_A/) + - [Pattern B - Provision Cloud Firestore with Cloud Run](./pattern_B/) + - [Pattern C - Compute Engine with Node.js](./pattern_C/) + - [Pattern D - Host a database backed website](./pattern_D/) 2. Apply Google Cloud resources with `terraform`: - ``` bash - terraform init // Initializes and installs all required modules - terraform plan // Displays preview of resources being applied to project - terraform apply // Executes application of resources - ``` + ```bash + terraform init // Initializes and installs all required modules + terraform plan // Displays preview of resources being applied to project + terraform apply // Executes application of resources + ``` To clean up, run `terraform destroy` to remove resources. diff --git a/other/train-to-cloud-city/terraform/pattern_A/README.md b/other/train-to-cloud-city/terraform/pattern_A/README.md index 554d5745..38d2406c 100644 --- a/other/train-to-cloud-city/terraform/pattern_A/README.md +++ b/other/train-to-cloud-city/terraform/pattern_A/README.md @@ -2,17 +2,17 @@ ### Requirements -* `gcloud` cli (To install [click here](https://cloud.google.com/sdk/docs/install)) -* `terraform` cli (To install [click here](https://developer.hashicorp.com/terraform/tutorials/gcp-get-started/install-cli)) -* Google Cloud Platform project (To create a project [click here](https://cloud.google.com/resource-manager/docs/creating-managing-projects#gcloud)) +- `gcloud` cli (To install [click here](https://cloud.google.com/sdk/docs/install)) +- `terraform` cli (To install [click here](https://developer.hashicorp.com/terraform/tutorials/gcp-get-started/install-cli)) +- Google Cloud Platform project (To create a project [click here](https://cloud.google.com/resource-manager/docs/creating-managing-projects#gcloud)) ## Technologies -* [Cloud Run](https://cloud.google.com/run) -* [Cloud Build](https://cloud.google.com/build) -* [Cloud Deploy](https://cloud.google.com/deploy) -* [Artifact Registry](https://cloud.google.com/artifact-registry) -* [Terraform](https://registry.terraform.io) +- [Cloud Run](https://cloud.google.com/run) +- [Cloud Build](https://cloud.google.com/build) +- [Cloud Deploy](https://cloud.google.com/deploy) +- [Artifact Registry](https://cloud.google.com/artifact-registry) +- [Terraform](https://registry.terraform.io) ## Instructions @@ -32,17 +32,17 @@ gcloud config set project 4. Initialize and apply terraform like so. -``` bash +```bash terraform init // Initializes and installs all required modules terraform plan // Displays preview of resources being applied to project terraform apply // Executes application of resources ``` 5. Once your set up is complete, you can test it by doing the following: - a. Add new `target/` directory in your connected repository. This should contain code with relevant `cloudbuild.yaml` or `Dockerfile`. - b. Pushing a test commit inside `target/` in your connected repository. - c. Watch the build in Cloud Build complete pushing an image to Artifact Registry. - d. Watch completion of push in Artifact Registry triggers `gcr` topic for Cloud Deploy to commence. + a. Add new `target/` directory in your connected repository. This should contain code with relevant `cloudbuild.yaml` or `Dockerfile`. + b. Pushing a test commit inside `target/` in your connected repository. + c. Watch the build in Cloud Build complete pushing an image to Artifact Registry. + d. Watch completion of push in Artifact Registry triggers `gcr` topic for Cloud Deploy to commence. 6. Clean up terraform resources by executing: @@ -52,6 +52,5 @@ terraform destroy ## What's next? -* Try out walkthrough using Cloud Deploy with Cloud Run [![Open in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.svg)](https://shell.cloud.google.com/?show=ide%2Cterminal&walkthrough_id=deploy--cloud-deploy-e2e-run) -* Try out walkthrough using Cloud Deploy with GKE [![Open in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.svg)](https://shell.cloud.google.com/?show=ide%2Cterminal&walkthrough_id=deploy--cloud-deploy-e2e-gke) - +- Try out walkthrough using Cloud Deploy with Cloud Run [![Open in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.svg)](https://shell.cloud.google.com/?show=ide%2Cterminal&walkthrough_id=deploy--cloud-deploy-e2e-run) +- Try out walkthrough using Cloud Deploy with GKE [![Open in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.svg)](https://shell.cloud.google.com/?show=ide%2Cterminal&walkthrough_id=deploy--cloud-deploy-e2e-gke) diff --git a/other/train-to-cloud-city/terraform/pattern_B/README.md b/other/train-to-cloud-city/terraform/pattern_B/README.md index 3cee2708..0b82e7d5 100644 --- a/other/train-to-cloud-city/terraform/pattern_B/README.md +++ b/other/train-to-cloud-city/terraform/pattern_B/README.md @@ -2,16 +2,16 @@ ### Requirements -* `gcloud` cli (To install [click here](https://cloud.google.com/sdk/docs/install)) -* `terraform` cli (To install [click here](https://developer.hashicorp.com/terraform/tutorials/gcp-get-started/install-cli)) -* Google Cloud Platform project (To create a project [click here](https://cloud.google.com/resource-manager/docs/creating-managing-projects#gcloud)) +- `gcloud` cli (To install [click here](https://cloud.google.com/sdk/docs/install)) +- `terraform` cli (To install [click here](https://developer.hashicorp.com/terraform/tutorials/gcp-get-started/install-cli)) +- Google Cloud Platform project (To create a project [click here](https://cloud.google.com/resource-manager/docs/creating-managing-projects#gcloud)) ## Technologies -* [Cloud Firestore](https://firebase.google.com/docs/firestore) -* [Cloud Run](https://cloud.google.com/run) -* [Cloud Build](https://cloud.google.com/build) -* [Artifact Registry](https://cloud.google.com/artifact-registry) +- [Cloud Firestore](https://firebase.google.com/docs/firestore) +- [Cloud Run](https://cloud.google.com/run) +- [Cloud Build](https://cloud.google.com/build) +- [Artifact Registry](https://cloud.google.com/artifact-registry) ## Instructions @@ -41,10 +41,10 @@ gcloud builds submit --region=us-central1 --tag us-central1-docker.pkg.dev//test-repo/image:tag1 ``` -5. *Result*: Open provided Cloud Run url to see client populate with the seeded data from Cloud Firestore. +5. _Result_: Open provided Cloud Run url to see client populate with the seeded data from Cloud Firestore. ## What's next? -* Learn more about [Firestore events to GKE via Eventarc triggers](https://cloud.google.com/eventarc/docs/gke/route-trigger-cloud-firestore) -* Learn more about [Firestore events to Cloud Run via Eventarc triggers](https://cloud.google.com/eventarc/docs/run/route-trigger-cloud-firestore) -* Learn more [Firestore events to Workflows via Eventarc triggers](https://cloud.google.com/eventarc/docs/workflows/route-trigger-cloud-firestore) +- Learn more about [Firestore events to GKE via Eventarc triggers](https://cloud.google.com/eventarc/docs/gke/route-trigger-cloud-firestore) +- Learn more about [Firestore events to Cloud Run via Eventarc triggers](https://cloud.google.com/eventarc/docs/run/route-trigger-cloud-firestore) +- Learn more [Firestore events to Workflows via Eventarc triggers](https://cloud.google.com/eventarc/docs/workflows/route-trigger-cloud-firestore) diff --git a/other/train-to-cloud-city/terraform/pattern_B/data/plants.json b/other/train-to-cloud-city/terraform/pattern_B/data/plants.json index a4481b0d..fcacbaed 100644 --- a/other/train-to-cloud-city/terraform/pattern_B/data/plants.json +++ b/other/train-to-cloud-city/terraform/pattern_B/data/plants.json @@ -1,21 +1,23 @@ -[{ - "collection": "plants", - "data": { - "name": "String of Hearts", - "type": "succulents" +[ + { + "collection": "plants", + "data": { + "name": "String of Hearts", + "type": "succulents" + } + }, + { + "collection": "plants", + "data": { + "name": "String of Pearls", + "type": "succulents" + } + }, + { + "collection": "plants", + "data": { + "name": "String of Rubies", + "type": "succulents" + } } -}, -{ - "collection": "plants", - "data": { - "name": "String of Pearls", - "type": "succulents" - } -}, -{ - "collection": "plants", - "data": { - "name": "String of Rubies", - "type": "succulents" - } -}] +] diff --git a/other/train-to-cloud-city/terraform/pattern_B/node-server/Dockerfile b/other/train-to-cloud-city/terraform/pattern_B/node-server/Dockerfile index 8ce39ef9..92d36ee9 100644 --- a/other/train-to-cloud-city/terraform/pattern_B/node-server/Dockerfile +++ b/other/train-to-cloud-city/terraform/pattern_B/node-server/Dockerfile @@ -1,16 +1,16 @@ -// Copyright 2024 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. FROM node:18.16.0-alpine3.17 diff --git a/other/train-to-cloud-city/terraform/pattern_B/node-server/main.js b/other/train-to-cloud-city/terraform/pattern_B/node-server/main.js index 46e715e3..fcf5ba0b 100644 --- a/other/train-to-cloud-city/terraform/pattern_B/node-server/main.js +++ b/other/train-to-cloud-city/terraform/pattern_B/node-server/main.js @@ -12,20 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -const {Firestore} = require('@google-cloud/firestore'); +const { Firestore } = require("@google-cloud/firestore"); const firestore = new Firestore(); (async function quickstart() { const ref = firestore.collection("plants"); let plants = []; - + try { const snapshot = await ref.get(); snapshot.docs.forEach((doc) => { plants.push(doc.data()); }); - } catch(error) { + } catch (error) { console.error(error); } })(); diff --git a/other/train-to-cloud-city/terraform/pattern_C/README.md b/other/train-to-cloud-city/terraform/pattern_C/README.md index 068c3022..b1f30cdb 100644 --- a/other/train-to-cloud-city/terraform/pattern_C/README.md +++ b/other/train-to-cloud-city/terraform/pattern_C/README.md @@ -2,14 +2,14 @@ ### Requirements -* `gcloud` cli (To install [click here](https://cloud.google.com/sdk/docs/install)) -* `terraform` cli (To install [click here](https://developer.hashicorp.com/terraform/tutorials/gcp-get-started/install-cli)) -* Google Cloud Platform project (To create a project [click here](https://cloud.google.com/resource-manager/docs/creating-managing-projects#gcloud)) +- `gcloud` cli (To install [click here](https://cloud.google.com/sdk/docs/install)) +- `terraform` cli (To install [click here](https://developer.hashicorp.com/terraform/tutorials/gcp-get-started/install-cli)) +- Google Cloud Platform project (To create a project [click here](https://cloud.google.com/resource-manager/docs/creating-managing-projects#gcloud)) ## Technologies -* [Compute Engine](https://cloud.google.com/compute) -* [Terraform](https://registry.terraform.io) +- [Compute Engine](https://cloud.google.com/compute) +- [Terraform](https://registry.terraform.io) ## Instructions @@ -27,13 +27,13 @@ gcloud config set project 3. Initialize and apply terraform like so: -``` bash +```bash terraform init // Initializes and installs all required modules terraform plan // Displays preview of resources being applied to project terraform apply // Executes application of resources ``` -4. Open up node provisioned [virtual machine `node-virtual-machine`](https://console.cloud.google.com/compute/instances) +4. Open up node provisioned [virtual machine `node-virtual-machine`](https://console.cloud.google.com/compute/instances) 5. click `SSH` option of `node-virtual-machine` vm instance. @@ -43,8 +43,8 @@ terraform apply // Executes application of resources touch server.js // create app.js and copy contents of this directory's `./app.js` node server.js // run server at port 5000 ``` -Continue to have the server running when you continue to the next step. +Continue to have the server running when you continue to the next step. 7. Execute the following in Cloud Shell to see the server response through an external url from Compute engine. @@ -60,4 +60,4 @@ terraform destroy ## What's next? -* [Deploy a Java application with Compute Engine](https://pantheon.corp.google.com/products/solutions/details/java-application) +- [Deploy a Java application with Compute Engine](https://pantheon.corp.google.com/products/solutions/details/java-application) diff --git a/other/train-to-cloud-city/terraform/pattern_C/server.js b/other/train-to-cloud-city/terraform/pattern_C/server.js index dd103527..e66fecad 100644 --- a/other/train-to-cloud-city/terraform/pattern_C/server.js +++ b/other/train-to-cloud-city/terraform/pattern_C/server.js @@ -17,8 +17,8 @@ const express = require("express"); const app = express(); const port = 5000; -app.get('/', (req, res) => { - res.send('Greetings Cloud Train!'); +app.get("/", (req, res) => { + res.send("Greetings Cloud Train!"); }); app.listen(port, () => { diff --git a/other/train-to-cloud-city/terraform/pattern_D/README.md b/other/train-to-cloud-city/terraform/pattern_D/README.md index 6fc57809..f1f5b271 100644 --- a/other/train-to-cloud-city/terraform/pattern_D/README.md +++ b/other/train-to-cloud-city/terraform/pattern_D/README.md @@ -3,4 +3,3 @@ ## Technologies ## What's next? -