diff --git a/packages/app/package.json b/packages/app/package.json
index d0c6eeafe2..d8a5e30d91 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -33,6 +33,7 @@
"@backstage/theme": "^0.2.16",
"@frontside/backstage-plugin-effection-inspector": "^0.1.2",
"@frontside/backstage-plugin-humanitec": "^0.3.1",
+ "@frontside/backstage-plugin-platform": "^0.1.0",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"history": "^5.0.0",
diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx
index 01754379e1..7d9067b8ec 100644
--- a/packages/app/src/App.tsx
+++ b/packages/app/src/App.tsx
@@ -33,6 +33,7 @@ import { FlatRoutes } from '@backstage/core-app-api';
import { orgPlugin } from '@backstage/plugin-org';
import { InspectorPage } from '@frontside/backstage-plugin-effection-inspector';
import { GraphiQLPage } from '@backstage/plugin-graphiql';
+import { PlatformPage } from '@frontside/backstage-plugin-platform';
const app = createApp({
apis,
@@ -90,6 +91,7 @@ const routes = (
} />
} />
} />
+ }/>
);
diff --git a/packages/backend/.eslintrc.js b/packages/backend/.eslintrc.js
index e2a53a6ad2..e28be0ab46 100644
--- a/packages/backend/.eslintrc.js
+++ b/packages/backend/.eslintrc.js
@@ -1 +1,5 @@
-module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
+module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, {
+ rules: {
+ 'prefer-const': 'off'
+ }
+});
diff --git a/packages/backend/.gitignore b/packages/backend/.gitignore
new file mode 100644
index 0000000000..ff278ebc46
--- /dev/null
+++ b/packages/backend/.gitignore
@@ -0,0 +1 @@
+/dist-bin/
diff --git a/packages/backend/package.json b/packages/backend/package.json
index 4dd7de56b3..36407876df 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -43,6 +43,7 @@
"@frontside/backstage-plugin-graphql": "^0.4.1",
"@frontside/backstage-plugin-incremental-ingestion-backend": "*",
"@frontside/backstage-plugin-incremental-ingestion-github": "*",
+ "@frontside/backstage-plugin-platform-backend": "*",
"graphql-modules": "^2.1.0",
"@gitbeaker/node": "^34.6.0",
"@internal/plugin-healthcheck": "0.1.1",
diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts
index 79890a0713..61cbe581a4 100644
--- a/packages/backend/src/index.ts
+++ b/packages/backend/src/index.ts
@@ -34,6 +34,7 @@ import healthcheck from './plugins/healthcheck';
import effectionInspector from './plugins/effection-inspector';
import humanitec from './plugins/humanitec';
import graphql from './plugins/graphql';
+import idp from './plugins/idp';
import { PluginEnvironment } from './types';
import { CatalogClient } from '@backstage/catalog-client';
@@ -99,6 +100,7 @@ async function main() {
const searchEnv = useHotMemoize(module, () => createEnv('search'));
const appEnv = useHotMemoize(module, () => createEnv('app'));
const humanitecEnv = useHotMemoize(module, () => createEnv('humanitec'));
+ const idpEnv = useHotMemoize(module, () => createEnv('idp'));
const apiRouter = Router();
apiRouter.use('/catalog', await catalog(catalogEnv));
@@ -111,6 +113,7 @@ async function main() {
apiRouter.use('/effection-inspector', await effectionInspector(effectionInspectorEnv));
apiRouter.use('/humanitec', await humanitec(humanitecEnv));
apiRouter.use('/graphql', await graphql(graphqlEnv));
+ apiRouter.use('/idp', await idp(idpEnv));
apiRouter.use(notFoundHandler());
const service = createServiceBuilder(module)
diff --git a/packages/backend/src/plugins/catalog.ts b/packages/backend/src/plugins/catalog.ts
index 61f287cba4..e09176c066 100644
--- a/packages/backend/src/plugins/catalog.ts
+++ b/packages/backend/src/plugins/catalog.ts
@@ -3,9 +3,7 @@ import {
} from '@backstage/plugin-catalog-backend';
import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backend';
import { IncrementalCatalogBuilder } from '@frontside/backstage-plugin-incremental-ingestion-backend';
-import { GithubRepositoryEntityProvider } from '@frontside/backstage-plugin-incremental-ingestion-github';
import { Router } from 'express';
-import { Duration } from 'luxon';
import { PluginEnvironment } from '../types';
export default async function createPlugin(
@@ -16,21 +14,6 @@ export default async function createPlugin(
// incremental builder receives builder because it'll register
// incremental entity providers with the builder
const incrementalBuilder = IncrementalCatalogBuilder.create(env, builder);
-
- const githubRepositoryProvider = GithubRepositoryEntityProvider.create({
- host: 'github.com',
- searchQuery: "created:>1970-01-01 user:thefrontside",
- config: env.config
- })
-
- incrementalBuilder.addIncrementalEntityProvider(
- githubRepositoryProvider,
- {
- burstInterval: Duration.fromObject({ seconds: 3 }),
- burstLength: Duration.fromObject({ seconds: 3 }),
- restLength: Duration.fromObject({ day: 1 })
- }
- )
builder.addProcessor(new ScaffolderEntitiesProcessor());
diff --git a/packages/backend/src/plugins/idp.ts b/packages/backend/src/plugins/idp.ts
new file mode 100644
index 0000000000..8b5ae33f94
--- /dev/null
+++ b/packages/backend/src/plugins/idp.ts
@@ -0,0 +1,78 @@
+import type { Router } from 'express';
+import { createRouter } from '@frontside/backstage-plugin-platform-backend';
+import { createHumanitecPlatformApi } from '@frontside/backstage-plugin-humanitec-common';
+import { PluginEnvironment } from '../types';
+import { Entity } from '@backstage/catalog-model';
+
+export default async function createPlugin({
+ config,
+ logger,
+ discovery,
+ catalog,
+}: PluginEnvironment): Promise {
+ return await createRouter({
+ executableName: 'idp',
+ logger,
+ discovery,
+ appURL: `${config.getString('app.baseUrl')}/platform`,
+ catalog,
+ platform: {
+ async getRepositoryUrls(ref) {
+ const entity = await ref.load();
+
+ if (entity) {
+ const slug = getGithubProjectSlug(entity);
+ return {
+ ssh: `git@github.com:${slug}.git`,
+ https: `https://github/${slug}.git`
+ }
+ }
+
+ return null;
+ },
+ async getRepositories() {
+ const { items: entities } = await catalog.getEntities();
+
+ const repositories = entities.flatMap(entity => {
+ const slug = getGithubProjectSlug(entity);
+ if (slug) {
+ return [{
+ componentRef: getComponentRef(entity),
+ slug,
+ description: entity.metadata.description,
+ url: `https://github/${slug}`,
+ }]
+ }
+ return [];
+ });
+ return {
+ hasNextPage: false,
+ hasPreviousPage: false,
+ beginCursor: '',
+ endCursor: '',
+ items: repositories.map(r => ({
+ cursor: '',
+ value: r
+ }))
+ }
+ },
+ ...createHumanitecPlatformApi({
+ token: config.getString('humanitec.token'),
+ }),
+ },
+ });
+}
+
+function getGithubProjectSlug(entity: Entity) {
+ return entity.metadata
+ && entity.metadata.annotations
+ && entity.metadata.annotations["github.com/project-slug"];
+}
+
+function getComponentRef(entity: Entity) {
+ return [
+ entity.kind !== 'Component' ? entity.kind.toLowerCase() : '',
+ entity.metadata.namespace && entity.metadata.namespace !== 'default' ? entity.metadata.namespace : '',
+ entity.metadata.name
+ ].join('')
+}
diff --git a/plugins/humanitec-common/src/clients/humanitec.ts b/plugins/humanitec-common/src/clients/humanitec.ts
index 7b07a37a49..7b5e80a093 100644
--- a/plugins/humanitec-common/src/clients/humanitec.ts
+++ b/plugins/humanitec-common/src/clients/humanitec.ts
@@ -60,6 +60,18 @@ export function createHumanitecClient({ orgId, token }: { token: string; orgId:
const result = await _fetch('GET', `apps/${appId}/envs/${envId}/resources`);
return ResourcesResponsePayload.parse(result);
},
+
+ async getEnvironmentLogsSnapshot(appId: string, envId: string) {
+ type Message = {
+ workload_id: string;
+ container_id: string;
+ payload: string;
+ level: string
+ };
+
+ return await _fetch('GET', `apps/${appId}/envs/${envId.toLowerCase()}/logs?limit=100&invert=true`);
+
+ },
buildUrl(params: URLs) {
const baseUrl = `https://app.humanitec.io/orgs/${orgId}`;
switch (params.resource) {
@@ -89,6 +101,8 @@ export function createHumanitecClient({ orgId, token }: { token: string; orgId:
if (r.ok) {
return await r.json() as R;
+ } else {
+ console.dir({ error: r.statusText });
}
throw new FetchError(`Fetch ${method} to ${url} failed.`, r);
@@ -98,4 +112,4 @@ export function createHumanitecClient({ orgId, token }: { token: string; orgId:
retry: async (e: FetchError) => e.status === 403
});
}
-}
\ No newline at end of file
+}
diff --git a/plugins/humanitec-common/src/constants.ts b/plugins/humanitec-common/src/constants.ts
new file mode 100644
index 0000000000..e94ede15d3
--- /dev/null
+++ b/plugins/humanitec-common/src/constants.ts
@@ -0,0 +1,2 @@
+export const HUMANITEC_ORG_ID_ANNOTATION = "humanitec.com/orgId";
+export const HUMANITEC_APP_ID_ANNOTATION = "humanitec.com/appId";
diff --git a/plugins/humanitec-common/src/index.ts b/plugins/humanitec-common/src/index.ts
index 51075d7dec..6bae1a7cb4 100644
--- a/plugins/humanitec-common/src/index.ts
+++ b/plugins/humanitec-common/src/index.ts
@@ -4,4 +4,6 @@ export * from './types/environment';
export * from './types/resources';
export * from './types/runtime';
export * from './clients/fetch-app-info';
-export * from './clients/humanitec';
\ No newline at end of file
+export * from './clients/humanitec';
+export * from './platform-api';
+export * from './constants';
diff --git a/plugins/humanitec-common/src/platform-api.ts b/plugins/humanitec-common/src/platform-api.ts
new file mode 100644
index 0000000000..86d91a9ad0
--- /dev/null
+++ b/plugins/humanitec-common/src/platform-api.ts
@@ -0,0 +1,68 @@
+import type { PlatformApi } from '@frontside/backstage-plugin-platform-backend';
+import type { Entity } from '@backstage/catalog-model';
+
+import { HUMANITEC_APP_ID_ANNOTATION, HUMANITEC_ORG_ID_ANNOTATION } from './constants';
+import { createHumanitecClient } from './clients/humanitec';
+
+
+export type HumanitecPlatformAPI = Pick
+export function createHumanitecPlatformApi({ token }: { token: string }): HumanitecPlatformAPI {
+
+ return {
+ async *getLogs(ref, envId) {
+ const entity = await ref.load();
+ const { appId, orgId } = getHumanitecMetadata(entity);
+ const client = createHumanitecClient({ token, orgId });
+ const logs = await client.getEnvironmentLogsSnapshot(appId, envId);
+ const bound = Math.floor(logs.length * .5);
+ const send = logs.slice(0, bound);
+ const stream = logs.slice(bound, logs.length);
+ for (const message of send) {
+ yield message.payload;
+ }
+ for (const message of stream) {
+ await new Promise((resolve) => setTimeout(resolve, Math.random() * 1500));
+ yield message.payload;
+ }
+ },
+
+ async getEnvironments(ref) {
+ const entity = await ref.load();
+ const { appId, orgId } = getHumanitecMetadata(entity);
+ const client = createHumanitecClient({ token, orgId });
+ const environments = await client.getEnvironments(appId);
+ return {
+ hasNextPage: false,
+ hasPreviousPage: false,
+ beginCursor: '',
+ endCursor: '',
+ items: environments.map(env => ({
+ cursor: '',
+ value: {
+ id: env.id,
+ name: env.name,
+ type: env.type,
+ url: `https://app.humanitec.io/orgs/${orgId}/apps/${appId}/envs/${env.id}`
+ }
+ })),
+ };
+ }
+ }
+}
+
+function getHumanitecMetadata(entity: Entity) {
+ const {
+ [HUMANITEC_ORG_ID_ANNOTATION]: orgId,
+ [HUMANITEC_APP_ID_ANNOTATION]: appId,
+ } = entity.metadata.annotations ?? {};
+
+ if (!orgId) {
+ throw new Error(`${entity.kind}:${entity.metadata.name} is not a humanitec entity`);
+
+ }
+ if (!appId) {
+ throw new Error(`${entity.kind}:${entity.metadata.name} is not a humanitec entity`);
+
+ }
+ return { orgId, appId };
+}
diff --git a/plugins/platform-backend/.eslintrc.js b/plugins/platform-backend/.eslintrc.js
new file mode 100644
index 0000000000..e28be0ab46
--- /dev/null
+++ b/plugins/platform-backend/.eslintrc.js
@@ -0,0 +1,5 @@
+module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, {
+ rules: {
+ 'prefer-const': 'off'
+ }
+});
diff --git a/plugins/platform-backend/README.md b/plugins/platform-backend/README.md
new file mode 100644
index 0000000000..feba2941e5
--- /dev/null
+++ b/plugins/platform-backend/README.md
@@ -0,0 +1,14 @@
+# platform-backend
+
+Welcome to the platform-backend backend plugin!
+
+_This plugin was created through the Backstage CLI_
+
+## Getting started
+
+Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn
+start` in the root directory, and then navigating to [/platform-backend](http://localhost:3000/platform-backend).
+
+You can also serve the plugin in isolation by running `yarn start` in the plugin directory.
+This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads.
+It is only meant for local development, and the setup for it can be found inside the [/dev](/dev) directory.
diff --git a/plugins/platform-backend/cli/.vscode/settings.json b/plugins/platform-backend/cli/.vscode/settings.json
new file mode 100644
index 0000000000..5e27b51276
--- /dev/null
+++ b/plugins/platform-backend/cli/.vscode/settings.json
@@ -0,0 +1,5 @@
+{
+ "deno.enable": true,
+ "deno.lint": false,
+ "deno.unstable": true
+}
\ No newline at end of file
diff --git a/plugins/platform-backend/cli/cli.ts b/plugins/platform-backend/cli/cli.ts
new file mode 100644
index 0000000000..c84311eaa9
--- /dev/null
+++ b/plugins/platform-backend/cli/cli.ts
@@ -0,0 +1,347 @@
+import {
+ assert,
+ blue,
+ Command,
+ Entity,
+ EventSource,
+ format,
+ green,
+ path,
+ readAll,
+ red,
+ yaml,
+} from "./deps.ts";
+
+export interface CLIOptions {
+ name: string;
+ description: string;
+ apiURL: string;
+ args: string[];
+ target: string;
+}
+
+interface SSEMessage {
+ type: "log" | "completion" | "error";
+ createdAt: string;
+ body: {
+ message: string;
+ error?: {
+ name: string;
+ message: string;
+ };
+ };
+}
+
+class MainError extends Error {
+ name = "Mainerror";
+}
+
+const logTextColors: Record string> = {
+ "log": blue,
+ "completion": green,
+ "error": red,
+};
+
+function logSSEMessage(raw: string) {
+ const message: SSEMessage = JSON.parse(raw);
+ const color = logTextColors[message.type];
+ const timestamp = format(new Date(message.createdAt), "dd-MM-yyyy:hh:mm");
+ const logType = color(`[${message.type.toLocaleUpperCase()} - ${timestamp}]`);
+
+ console.log(`${logType} - ${message.body.message})`);
+
+ if (message.body.error) {
+ logSSEMessage(JSON.stringify({
+ type: "error",
+ body: {
+ message: message.body.error.message,
+ },
+ createdAt: message.createdAt,
+ }));
+ }
+}
+
+export async function cli(options: CLIOptions) {
+ let { apiURL, description, args, name, target } = options;
+ let get: typeof fetch = (endpoint, init) =>
+ fetch(`${apiURL}/${endpoint}`, init);
+
+ let post = (endpoint: string, init: Omit) =>
+ fetch(`${apiURL}/${endpoint}`, {
+ method: "POST",
+ ...init,
+ });
+
+ const cmd = new Command()
+ .name(name)
+ .version(() => `\narchitecture: ${target}\nbackstage: ${apiURL}`)
+ .description(description)
+ .command("info", "display info about a backstage component entity.")
+ .option(
+ "-c --component ",
+ "The backstage component entity",
+ )
+ .action(async ({ component }) => {
+ let ref = await findEntityContext(component);
+ let response: Response;
+ try {
+ response = await get(`components/${ref}/info`);
+ } catch (error) {
+ throw new MainError(error.message);
+ }
+ if (!response) {
+ throw new MainError(`no response from server`);
+ } else if (response.ok) {
+ await Deno.stdout.write(
+ new TextEncoder().encode(await response.text()),
+ );
+ } else {
+ if (response.status === 404) {
+ throw new MainError(`unknown component '${ref}'`);
+ } else {
+ throw new MainError(
+ `communication error with backstage server: ${response.status} ${response.statusText}`,
+ );
+ }
+ }
+ })
+ .command(
+ "clone",
+ "clone a repository associated with a component",
+ )
+ .option("-S, --ssh", "Use ssh url to clone repository")
+ .option("-H, --https", "Use https url to clone repository")
+ .arguments(
+ "[component:string] [directory:string]",
+ )
+ .action(async ({ ssh, https }, component, directory) => {
+ const output = directory ?? component;
+ if (ssh && https) {
+ throw new MainError(
+ `Invalid options: --ssh and --https can't be used together - use one or the other.`,
+ );
+ }
+ // prefer ssh
+ const protocol = !(ssh && https) || ssh ? "ssh" : "https";
+ if (component) {
+ let response: Response;
+ try {
+ response = await get(`repositories/${component}/urls`);
+ } catch (error) {
+ throw new MainError(error.message);
+ }
+ if (response.ok) {
+ const urls = await response.json();
+ const url = urls[protocol];
+ const clone = Deno.run({
+ cmd: ["git", "clone", url, output],
+ });
+ if (!(await clone.status()).success) {
+ throw new MainError(
+ `Encountered an error cloning "${url}" to "${output}".`,
+ );
+ }
+ } else if (response.status === 404) {
+ throw new MainError(`unknown component '${component}'`);
+ } else {
+ throw new MainError(
+ `communication error with backstage server: ${response.status} ${response.statusText}`,
+ );
+ }
+ return;
+ }
+
+ let response: Response;
+ try {
+ response = await get(`repositories`, {
+ headers: {
+ Accept: "text/plain",
+ },
+ });
+ } catch (error) {
+ throw new MainError(error.message);
+ }
+ if (response.ok) {
+ await Deno.stdout.write(
+ new TextEncoder().encode(await response.text()),
+ );
+ } else {
+ throw new MainError(
+ `communication error with backstage server: ${response.status} ${response.statusText}`,
+ );
+ }
+ })
+ .command(
+ "environments",
+ "list enviroments in which a component is deployed",
+ )
+ .option(
+ "-c --component ",
+ "The backstage component entity",
+ )
+ .action(async ({ component }) => {
+ let ref = await findEntityContext(component);
+ let response: Response;
+ try {
+ response = await get(`components/${ref}/environments`);
+ } catch (error) {
+ throw new MainError(error.message);
+ }
+ if (!response) {
+ throw new MainError(`no response from server`);
+ } else if (response.ok) {
+ await Deno.stdout.write(
+ new TextEncoder().encode(await response.text()),
+ );
+ } else {
+ if (response.status === 404) {
+ throw new MainError(`unknown component '${ref}'`);
+ } else {
+ throw new MainError(
+ `communication error with backstage server: ${response.status} ${response.statusText}`,
+ );
+ }
+ }
+ })
+ .command(
+ "create",
+ `create something new from a template.
+usage:
+
+# heredoc
+<", "the scaffolder template", {
+ default: "standard-microservice",
+ })
+ .option(
+ "-f --file ",
+ `an optional file path to a file containing the template's fields`,
+ )
+ .arguments("[input]")
+ .action(async ({ template, file }, input) => {
+ let body: string | undefined;
+
+ if (input === "-") {
+ const stdinContent = await readAll(Deno.stdin);
+ body = new TextDecoder().decode(stdinContent);
+ } else if (file) {
+ body = Deno.readTextFileSync(file);
+ }
+
+ assert(body, `no body has been created.`);
+
+ const response = await post(`create/${template}`, {
+ headers: {
+ "Content-Type": "text/plain",
+ },
+ body,
+ });
+
+ if (response.status !== 200) {
+ throw new MainError(
+ `create failed with ${response.status} - ${response.statusText}`,
+ );
+ }
+
+ // deno-lint-ignore no-explicit-any
+ function sseMessageHandler(event: any) {
+ if (event.data) {
+ try {
+ logSSEMessage(event.data);
+ } catch (ex) {
+ console.error(ex);
+ }
+ }
+ }
+
+ const { taskId } = await response.json();
+
+ const eventSourceUrl = `${apiURL}/tasks/${taskId}/eventstream`;
+
+ const eventSource = new EventSource(eventSourceUrl, {
+ withCredentials: true,
+ });
+
+ eventSource.addEventListener("log", sseMessageHandler);
+ eventSource.addEventListener("completion", (event: any) => {
+ sseMessageHandler(event);
+
+ eventSource.close();
+ });
+ eventSource.addEventListener("error", sseMessageHandler);
+ })
+ .command("logs")
+ .option(
+ "-c --component ",
+ "the component for which to fetch the logs",
+ )
+ .action(async ({ component }) => {
+ let name = await findEntityContext(component);
+ let response = await get(`logs/${name}`);
+ await response.body?.pipeTo(Deno.stdout.writable);
+ });
+
+ try {
+ await cmd.parse(args);
+ } catch (error) {
+ if (error instanceof MainError) {
+ console.log(error.message);
+ } else {
+ throw error;
+ }
+ }
+}
+
+async function findEntityContext(component?: string): Promise {
+ if (component) {
+ return component;
+ }
+ let catalogInfoYaml = await findAndRead(
+ "catalog-info.yml",
+ "catalog-info.yaml",
+ );
+ if (catalogInfoYaml.found) {
+ let [info] = yaml.parseAll(catalogInfoYaml.content) as Iterable;
+ if (info && info.metadata?.name) {
+ return info.metadata.name;
+ }
+ } else {
+ throw new MainError(
+ "unable to determine the component. You can set it explicitly by passing the `--component` flag",
+ );
+ }
+
+ return "";
+}
+
+type Find = {
+ found: false;
+} | {
+ found: true;
+ content: string;
+};
+async function findAndRead(...paths: string[]): Promise {
+ for (let cwd = Deno.cwd(); cwd !== "/"; cwd = path.join(cwd, "..")) {
+ for (let path of paths) {
+ try {
+ let content = new TextDecoder().decode(
+ await Deno.readFile(`${cwd}/${path}`),
+ );
+ return { found: true, content };
+ } catch (error) {
+ if (!(error instanceof Deno.errors.NotFound)) {
+ throw error;
+ }
+ }
+ }
+ }
+ return { found: false };
+}
diff --git a/plugins/platform-backend/cli/deno.json b/plugins/platform-backend/cli/deno.json
new file mode 100644
index 0000000000..a1cea42e11
--- /dev/null
+++ b/plugins/platform-backend/cli/deno.json
@@ -0,0 +1,10 @@
+{
+ "tasks": {
+ "dev": "deno run --location=http://localhost:7007 --allow-env --allow-read --allow-net=localhost:7007 --allow-run tasks/dev.ts"
+ },
+ "lint": {
+ "rules": {
+ "exclude": ["prefer-const"]
+ }
+ }
+}
diff --git a/plugins/platform-backend/cli/deps.ts b/plugins/platform-backend/cli/deps.ts
new file mode 100644
index 0000000000..78f79a57dc
--- /dev/null
+++ b/plugins/platform-backend/cli/deps.ts
@@ -0,0 +1,116 @@
+export { assert } from "https://deno.land/std@0.159.0/testing/asserts.ts";
+export * as path from "https://deno.land/std@0.159.0/path/mod.ts";
+export * as yaml from "https://deno.land/std@0.159.0/encoding/yaml.ts";
+export { format } from "https://deno.land/std@0.159.0/datetime/mod.ts";
+export { Command } from "https://deno.land/x/cliffy@v0.25.2/command/mod.ts";
+export { EventSource } from "https://deno.land/x/eventsource@v0.0.2/mod.ts";
+export { blue, green, red } from "https://deno.land/std@0.159.0/fmt/colors.ts";
+export { readAll } from "https://deno.land/std@0.159.0/streams/conversion.ts?s=copy";
+
+// we tried to get this from backstage. We really did.
+export interface Entity {
+ /**
+ * The version of specification format for this particular entity that
+ * this is written against.
+ */
+ apiVersion: string;
+
+ /**
+ * The high level entity type being described.
+ */
+ kind: string;
+
+ /**
+ * Metadata related to the entity.
+ */
+ metadata: EntityMeta;
+
+ /**
+ * The specification data describing the entity itself.
+ */
+ spec?: Record;
+}
+
+export interface EntityMeta {
+ /**
+ * A globally unique ID for the entity.
+ *
+ * This field can not be set by the user at creation time, and the server
+ * will reject an attempt to do so. The field will be populated in read
+ * operations. The field can (optionally) be specified when performing
+ * update or delete operations, but the server is free to reject requests
+ * that do so in such a way that it breaks semantics.
+ */
+ uid?: string;
+
+ /**
+ * An opaque string that changes for each update operation to any part of
+ * the entity, including metadata.
+ *
+ * This field can not be set by the user at creation time, and the server
+ * will reject an attempt to do so. The field will be populated in read
+ * operations. The field can (optionally) be specified when performing
+ * update or delete operations, and the server will then reject the
+ * operation if it does not match the current stored value.
+ */
+ etag?: string;
+
+ /**
+ * The name of the entity.
+ *
+ * Must be unique within the catalog at any given point in time, for any
+ * given namespace + kind pair. This value is part of the technical
+ * identifier of the entity, and as such it will appear in URLs, database
+ * tables, entity references, and similar. It is subject to restrictions
+ * regarding what characters are allowed.
+ *
+ * If you want to use a different, more human readable string with fewer
+ * restrictions on it in user interfaces, see the `title` field below.
+ */
+ name: string;
+
+ /**
+ * The namespace that the entity belongs to.
+ */
+ namespace?: string;
+
+ /**
+ * A display name of the entity, to be presented in user interfaces instead
+ * of the `name` property above, when available.
+ *
+ * This field is sometimes useful when the `name` is cumbersome or ends up
+ * being perceived as overly technical. The title generally does not have
+ * as stringent format requirements on it, so it may contain special
+ * characters and be more explanatory. Do keep it very short though, and
+ * avoid situations where a title can be confused with the name of another
+ * entity, or where two entities share a title.
+ *
+ * Note that this is only for display purposes, and may be ignored by some
+ * parts of the code. Entity references still always make use of the `name`
+ * property, not the title.
+ */
+ title?: string;
+
+ /**
+ * A short (typically relatively few words, on one line) description of the
+ * entity.
+ */
+ description?: string;
+
+ /**
+ * Key/value pairs of identifying information attached to the entity.
+ */
+ labels?: Record;
+
+ /**
+ * Key/value pairs of non-identifying auxiliary information attached to the
+ * entity.
+ */
+ annotations?: Record;
+
+ /**
+ * A list of single-valued strings, to for example classify catalog entities in
+ * various ways.
+ */
+ tags?: string[];
+}
diff --git a/plugins/platform-backend/cli/install.sh b/plugins/platform-backend/cli/install.sh
new file mode 100644
index 0000000000..64ee2d2d02
--- /dev/null
+++ b/plugins/platform-backend/cli/install.sh
@@ -0,0 +1,219 @@
+executable_name() {
+ echo "{{executableName}}"
+}
+
+executables_url() {
+ echo "{{appURL}}"
+}
+
+downloads_url() {
+ echo "{{downloadsURL}}"
+}
+
+info() {
+ local action="$1"
+ local details="$2"
+ command printf '\033[1;32m%12s\033[0m %s\n' "$action" "$details" 1>&2
+}
+
+error() {
+ command printf '\033[1;31mError\033[0m: %s\n\n' "$1" 1>&2
+}
+
+warning() {
+ command printf '\033[1;33mWarning\033[0m: %s\n\n' "$1" 1>&2
+}
+
+request() {
+ command printf '\033[1m%s\033[0m\n' "$1" 1>&2
+}
+
+eprintf() {
+ command printf '%s\n' "$1" 1>&2
+}
+
+bold() {
+ command printf '\033[1m%s\033[0m' "$1"
+}
+
+usage() {
+ cat >&2 </dev/null
+
+# default to running setup after installing
+should_run_setup="true"
+
+# install to IDP_HOME, defaulting to ~/.volta
+install_dir="${IDP_HOME:-"$HOME/.idp"}"
+
+# parse command line options
+while [ $# -gt 0 ]
+do
+ arg="$1"
+
+ case "$arg" in
+ -h|--help)
+ usage
+ exit 0
+ ;;
+ --skip-setup)
+ shift # shift off the argument
+ should_run_setup="false"
+ ;;
+ *)
+ error "unknown option: '$arg'"
+ usage
+ exit 1
+ ;;
+ esac
+done
+
+install "$install_dir" "$should_run_setup"
diff --git a/plugins/platform-backend/cli/main.ts b/plugins/platform-backend/cli/main.ts
new file mode 100644
index 0000000000..d225bb52ac
--- /dev/null
+++ b/plugins/platform-backend/cli/main.ts
@@ -0,0 +1,16 @@
+import { assert } from "./deps.ts";
+import { cli } from "./cli.ts";
+
+let [name, apiURL, description, ...args] = Deno.args;
+
+assert(name, "compiled incorrectly - executable name is not defined");
+assert(apiURL, "compiled incorrectly - backstage platform url is not found");
+assert(description, "compiled incorrectly - no platform description defined");
+
+await cli({
+ name,
+ description,
+ apiURL,
+ args,
+ target: Deno.build.target,
+}).catch((error) => console.error(error));
diff --git a/plugins/platform-backend/cli/tasks/dev.ts b/plugins/platform-backend/cli/tasks/dev.ts
new file mode 100644
index 0000000000..313e81051c
--- /dev/null
+++ b/plugins/platform-backend/cli/tasks/dev.ts
@@ -0,0 +1,9 @@
+import { cli } from "../cli.ts";
+
+await cli({
+ name: "idp",
+ description: "internal developer platform ",
+ apiURL: "http://localhost:7007/api/idp",
+ args: Deno.args,
+ target: Deno.build.target,
+}).catch((error) => console.error(error));
diff --git a/plugins/platform-backend/package.json b/plugins/platform-backend/package.json
new file mode 100644
index 0000000000..7a84d216c9
--- /dev/null
+++ b/plugins/platform-backend/package.json
@@ -0,0 +1,56 @@
+{
+ "name": "@frontside/backstage-plugin-platform-backend",
+ "version": "0.1.0",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "license": "Apache-2.0",
+ "private": true,
+ "publishConfig": {
+ "access": "public",
+ "main": "dist/index.cjs.js",
+ "types": "dist/index.d.ts"
+ },
+ "backstage": {
+ "role": "backend-plugin"
+ },
+ "scripts": {
+ "start": "backstage-cli package start",
+ "build": "backstage-cli package build",
+ "lint": "backstage-cli package lint",
+ "test": "backstage-cli package test",
+ "clean": "backstage-cli package clean",
+ "prepack": "backstage-cli package prepack",
+ "postpack": "backstage-cli package postpack"
+ },
+ "dependencies": {
+ "@backstage/backend-common": "^0.15.2",
+ "@backstage/config": "^1.0.1",
+ "@types/express": "*",
+ "cli-table3": "^0.6.3",
+ "chalk": "^4.1.2",
+ "express": "^4.17.1",
+ "express-promise-router": "^4.1.0",
+ "js-yaml": "^4.1.0",
+ "node-fetch-native": "^0.1.7",
+ "node-deno": "^0.1.0",
+ "node-fetch": "^2.6.7",
+ "nunjucks": "^3.2.3",
+ "request": "^2.88.2",
+ "winston": "^3.2.1",
+ "yn": "^4.0.0"
+ },
+ "devDependencies": {
+ "@backstage/cli": "^0.20.0",
+ "@types/nunjucks": "^3.2.1",
+ "@types/supertest": "^2.0.8",
+ "@types/request": "^2.48.8",
+ "msw": "^0.42.0",
+ "supertest": "^4.0.2"
+ },
+ "files": [
+ "dist"
+ ],
+ "volta": {
+ "extends": "../../package.json"
+ }
+}
diff --git a/plugins/platform-backend/src/executables.ts b/plugins/platform-backend/src/executables.ts
new file mode 100644
index 0000000000..d8d0656bf5
--- /dev/null
+++ b/plugins/platform-backend/src/executables.ts
@@ -0,0 +1,184 @@
+import type { Logger } from 'winston';
+import type { CompilationTarget } from 'node-deno';
+import { CompilationTargets, run, compile } from 'node-deno';
+import { existsSync } from 'fs';
+
+export interface DownloadInfo {
+ executableName: string;
+ helpText: Async;
+ executables: Executables;
+}
+
+export type Executables = Record;
+
+type Async = {
+ "type": "pending";
+} | {
+ "type": "resolved";
+ value: T;
+} | {
+ "type": "rejected";
+ error: Error;
+}
+
+export type Executable = {
+ type: 'error';
+ error: Error;
+} | {
+ type: 'failure',
+ stderr: string;
+ stdout: string;
+} | {
+ type: 'compiled';
+ url: string;
+ stderr: string;
+ stdout: string;
+} | {
+ type: 'compiling';
+ stderr: string;
+ stdout: string;
+}
+
+export interface FindOrCreateOptions {
+ baseURL: string;
+ downloadsURL: string;
+ logger: Logger;
+ distDir: string;
+ executableName: string;
+ entrypoint: string;
+}
+
+export function getDownloadInfo(options: FindOrCreateOptions): DownloadInfo {
+ let { executableName } = options;
+
+ let executables = findOrCreateExecutables(options);
+
+ let helpText = getHelpText(options);
+
+ return { executableName, helpText, executables };
+}
+
+export function getHelpText(options: FindOrCreateOptions): Async {
+ let { executableName, entrypoint, baseURL, logger } = options;
+ let description = "internal developer platform";
+
+ let value: Async = { "type": "pending" };
+
+ run({
+ entrypoint: [entrypoint, executableName, baseURL, `"${description}"`, "--help"],
+ }).then(result => {
+ if (result.code != 0) {
+ logger.info(`help text generation failed: ${result.stderr}`);
+ value = { "type": "rejected", error: new Error(result.stderr) };
+ } else {
+ logger.info("help text generated for platform executable");
+ value = { "type": "resolved", value: result.stdout };
+ }
+ }).catch(error => {
+ logger.error(`help text generation failed: ${error}`);
+ value = { "type": "rejected", error };
+ });
+
+ return new Proxy({} as Async, {
+ get(_, prop: keyof Executable) {
+ return value[prop];
+ },
+ ownKeys: () => Object.keys(value),
+ getOwnPropertyDescriptor: (_, key) => ({
+ value: value[key as keyof Executable],
+ enumerable: true,
+ configurable: true,
+ })
+ })
+}
+
+export function findOrCreateExecutables(options: FindOrCreateOptions): Executables {
+ options.logger.info(`generating executables for ${options.executableName}`);
+ return CompilationTargets.reduce((executables, target) => {
+ return {
+ ...executables,
+ [target]: findOrCreateExecutable(target, options),
+ }
+ }, {}) as Executables;
+}
+
+function findOrCreateExecutable(target: CompilationTarget, options: FindOrCreateOptions): Executable {
+ let { logger, baseURL, distDir, executableName, entrypoint, downloadsURL } = options;
+ let output = `${distDir}/${executableName}-${target}`;
+ let url = `${downloadsURL}/${executableName}-${target}`;
+
+ let executable: Executable = {
+ type: 'compiling',
+ stdout: '',
+ stderr: '',
+ }
+
+ let ext = target.includes('windows') ? '.exe' : ''
+
+ if (existsSync(`${output}${ext}`)) {
+ logger.info(`found existing executable: ${output}`);
+ executable = {
+ type: 'compiled',
+ url,
+ stdout: '',
+ stderr: '',
+ }
+ } else {
+ logger.info(`compiling ${executableName} for ${target}`);
+ compile({
+ target,
+ output,
+ // pass metadata as the first three args of the script
+ entrypoint: [entrypoint, executableName, baseURL, '"internal developer platform"'],
+
+ // only allow network access back to the backstage server
+ allowNet: [new URL(baseURL).host],
+
+ allowRead: true,
+
+ allowRun: true,
+
+ location: baseURL,
+
+ // fail immediately if a permission is not present
+ noPrompt: true,
+ }).then(result => {
+ let stdio = {
+ stdout: result.stdout,
+ stderr: result.stderr,
+ };
+ if (result.code !== 0) {
+ logger.error(`compilation for ${target} failed: ${stdio.stderr}`);
+ executable = {
+ type: 'failure',
+ ...stdio,
+ }
+ } else {
+ logger.info(`compilation complete: ${output}`);
+ executable = {
+ type: 'compiled',
+ url,
+ ...stdio,
+ }
+ }
+ }).catch(error => {
+ logger.error(`compilation error: ${error}`);
+ executable = {
+ type: 'error',
+ error
+ }
+ });
+ }
+
+ return new Proxy({} as Executable, {
+ get(_, prop: keyof Executable) {
+ return executable[prop];
+ },
+ ownKeys: () => Object.keys(executable),
+ getOwnPropertyDescriptor: (_, key) => ({
+ value: executable[key as keyof Executable],
+ enumerable: true,
+ configurable: true,
+ })
+ });
+}
diff --git a/plugins/platform-backend/src/index.ts b/plugins/platform-backend/src/index.ts
new file mode 100644
index 0000000000..35b4d00bc4
--- /dev/null
+++ b/plugins/platform-backend/src/index.ts
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2020 The Backstage Authors
+ *
+ * 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.
+ */
+
+export * from './service/router';
+export * from './types';
+export type { DownloadInfo, Executables } from './executables';
diff --git a/plugins/platform-backend/src/service/router.ts b/plugins/platform-backend/src/service/router.ts
new file mode 100644
index 0000000000..441e180249
--- /dev/null
+++ b/plugins/platform-backend/src/service/router.ts
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2020 The Backstage Authors
+ *
+ * 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 { errorHandler, PluginEndpointDiscovery, resolvePackagePath } from '@backstage/backend-common';
+import type { CatalogClient } from '@backstage/catalog-client';
+import express from 'express';
+import Router from 'express-promise-router';
+import { readFile } from 'fs/promises';
+import { load } from 'js-yaml';
+import fetch from 'node-fetch-native';
+import * as nunjucks from 'nunjucks';
+import request from 'request';
+import type { Logger } from 'winston';
+import { getDownloadInfo } from '../executables';
+import { GetComponentRef, PlatformApi } from '../types';
+import { Repositories } from './routes/repositories';
+import { Logs } from './routes/logs';
+import CliTable3 from 'cli-table3';
+import chalk from 'chalk';
+
+export interface RouterOptions {
+ logger: Logger;
+ discovery: PluginEndpointDiscovery;
+ executableName: string;
+ appURL: string;
+ catalog: CatalogClient;
+ platform: PlatformApi;
+}
+
+export async function createRouter(
+ options: RouterOptions,
+ ): Promise {
+ const { catalog, logger, discovery, executableName, appURL, platform } = options;
+
+ let baseURL = await discovery.getBaseUrl('idp');
+ let downloadsURL = `${baseURL}/executables/dist`;
+ let scaffolderUrl = `${await discovery.getBaseUrl('scaffolder')}/v2/tasks`;
+
+ let executables = getDownloadInfo({
+ logger,
+ distDir: 'dist-bin',
+ baseURL,
+ downloadsURL,
+ executableName,
+ entrypoint: resolvePackagePath("@frontside/backstage-plugin-platform-backend", "cli", "main.ts"),
+ });
+
+ const getComponentRef: GetComponentRef = async (name) => {
+ return {
+ ref: `component:default/${name}`,
+ compound: {
+ kind: 'component',
+ name,
+ namespace: 'default'
+ },
+ load: async () => {
+ const entity = await catalog.getEntityByRef(`component:default/${name}`);
+ if (!entity) {
+ throw new Error(`Component ${name} not found.`);
+ }
+ return entity;
+ },
+ };
+ }
+
+ const router = Router();
+ router.use(express.text());
+ router.use(express.json());
+
+ router.get('/health', (_, response) => {
+ logger.info('PONG!');
+ response.send({ status: 'ok' });
+ });
+
+ router.get('/install.sh', async (_, response) => {
+ response.setHeader('Content-Type', 'text/plain');
+ let installerBytes = await readFile(resolvePackagePath("@frontside/backstage-plugin-platform-backend", "cli", "install.sh"));
+ response.send(nunjucks.renderString(String(installerBytes), {
+ appURL,
+ downloadsURL,
+ executableName,
+ }));
+ });
+
+ router.get('/executables', (_, response)=> {
+ response.send(executables);
+ });
+
+ router.use('/executables/dist', express.static('dist-bin'));
+
+ router.get('/components/:name/info', async (req, res) => {
+ let name = req.params.name;
+ let component = await catalog.getEntityByRef(`component:default/${name}`);
+ if (component) {
+ res.send(`${JSON.stringify(component, null, 2)}\n`);
+ } else {
+ res.sendStatus(404);
+ res.send("Not Found");
+ }
+ })
+
+ router.get('/components/:name/environments', async (req, res) => {
+ let name = req.params.name;
+ let ref = await getComponentRef(name);
+ if (ref) {
+ const table = new CliTable3({
+ head: ["ID", "Name", "Type", "URL"]
+ });
+ let environments = await platform.getEnvironments(ref);
+ table.push(
+ ...environments.items.map(({ value: r }) => ([r.id, r.name, r.type, r.url ]))
+ )
+ res.send(`\n${chalk.bold(' ☀️ Deployment Environments')}\n${table}\n`);
+ } else {
+ res.sendStatus(404);
+ res.send("Not Found");
+ }
+ });
+
+ router.post('/create/:template', async (req, res) => {
+ const template = req.params.template;
+
+ logger.info(`creating template ${template}`);
+
+ try {
+ const values = load(req.body) as Record;
+
+ const post = await fetch(scaffolderUrl, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ templateRef: `template:default/${template}`,
+ values: {
+ ...values
+ },
+ secrets: {}
+ })
+ });
+
+ if(post.status !== 201) {
+ throw new Error(`resource not created, ${post.status} - ${post.statusText}`);
+ }
+
+ const { id } = (await post.json()) as { id: string };
+
+ res.json({ taskId: id });
+ } catch(err) {
+ logger.error(err);
+ res.status(500);
+ res.render('error', { error: err })
+ }
+ });
+
+ router.get('/tasks/:taskId/eventstream', (req, res) => {
+ const { taskId } = req.params;
+
+ const eventStreamUrl = `${scaffolderUrl}/${encodeURIComponent(taskId)}/eventstream`
+
+ req.pipe(request(eventStreamUrl)).pipe(res);
+ });
+
+ router.use('/repositories', Repositories({
+ getComponentRef,
+ platform,
+ catalog
+ }));
+
+ router.use('/logs', Logs({
+ getComponentRef,
+ platform,
+ catalog
+ }));
+
+ router.use(errorHandler());
+ return router;
+}
diff --git a/plugins/platform-backend/src/service/routes/logs.ts b/plugins/platform-backend/src/service/routes/logs.ts
new file mode 100644
index 0000000000..6dc9ed53d5
--- /dev/null
+++ b/plugins/platform-backend/src/service/routes/logs.ts
@@ -0,0 +1,51 @@
+import type { CatalogClient } from '@backstage/catalog-client';
+import Router from 'express-promise-router';
+import express from 'express';
+import { GetComponentRef, PlatformApi } from '../../types';
+
+interface RouteOptions {
+ platform: PlatformApi;
+ catalog: CatalogClient;
+ getComponentRef: GetComponentRef;
+}
+
+type Route = (options: RouteOptions) => express.Router;
+
+export const Logs: Route = ({ platform, getComponentRef }) => {
+ const router = Router();
+
+ router.get('/:component', async (req, response) => {
+ // Mandatory headers and http status to keep connection open
+ response.writeHead(200, {
+ 'Connection': 'keep-alive',
+ 'Cache-Control': 'no-cache',
+ 'Content-Type': 'text/event-stream',
+ });
+
+ let closed = false;
+
+ req.on('close', () => {
+ closed = true;
+ });
+
+ let ref = await getComponentRef(req.params.component);
+ for await (const line of platform.getLogs(ref, "Development")) {
+ if (closed) {
+ return;
+ } else {
+ response.write(`${line}\n`);
+ flush(response);
+ }
+ }
+ });
+
+
+ return router;
+}
+
+function flush(response: express.Response) {
+ const flushable = response as unknown as { flush: Function };
+ if (typeof flushable.flush === 'function') {
+ flushable.flush();
+ }
+}
diff --git a/plugins/platform-backend/src/service/routes/repositories.ts b/plugins/platform-backend/src/service/routes/repositories.ts
new file mode 100644
index 0000000000..24eea9223a
--- /dev/null
+++ b/plugins/platform-backend/src/service/routes/repositories.ts
@@ -0,0 +1,52 @@
+import type { CatalogClient } from '@backstage/catalog-client';
+import CliTable3 from 'cli-table3';
+import chalk from 'chalk';
+import Router from 'express-promise-router';
+import express from 'express';
+import { GetComponentRef, PlatformApi } from '../../types';
+
+interface RouteOptions {
+ platform: PlatformApi;
+ catalog: CatalogClient;
+ getComponentRef: GetComponentRef;
+}
+
+type Route = (options: RouteOptions) => express.Router;
+
+export const Repositories: Route = ({ platform, getComponentRef }) => {
+ const router = Router();
+
+ router.get('/', async (req, res) => {
+ const repositories = await platform.getRepositories();
+
+ if (req.accepts('json')) {
+ res.json(repositories);
+ } else {
+ const table = new CliTable3({
+ head: ['Component', 'Repository URL', 'Description']
+ });
+ table.push(
+ ...repositories.items.map(({ value: r }) => ([r.componentRef, r.url, r.description]))
+ )
+ res.send(`\n${chalk.bold(' 🥁 Available Repositories')}\n${table}`)
+ }
+
+ return router;
+ });
+
+ router.get('/:component/urls', async (req, res) => {
+ const name = req.params.component;
+
+ const ref = await getComponentRef(name);
+ const urls = await platform.getRepositoryUrls(ref);
+
+ if (urls) {
+ res.json(urls)
+ } else {
+ res.sendStatus(404);
+ res.send("Not Found");
+ }
+ });
+
+ return router;
+}
diff --git a/plugins/platform-backend/src/setupTests.ts b/plugins/platform-backend/src/setupTests.ts
new file mode 100644
index 0000000000..d3232290a7
--- /dev/null
+++ b/plugins/platform-backend/src/setupTests.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2020 The Backstage Authors
+ *
+ * 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.
+ */
+
+export {};
diff --git a/plugins/platform-backend/src/types.ts b/plugins/platform-backend/src/types.ts
new file mode 100644
index 0000000000..7e03c38db2
--- /dev/null
+++ b/plugins/platform-backend/src/types.ts
@@ -0,0 +1,54 @@
+import type { Entity, CompoundEntityRef } from '@backstage/catalog-model';
+
+export interface Environment {
+ id: string;
+ name: string;
+ type: string;
+ url: string;
+}
+
+export interface Repository {
+ componentRef: string;
+ slug: string;
+ description?: string;
+ url: string;
+}
+
+export interface RepositoryUrls {
+ ssh: string;
+ https: string;
+}
+
+export interface PlatformApi {
+ getLogs(ref: EntityRef, environment: string): AsyncIterable;
+ getEnvironments(ref: EntityRef, page?: PageSpec): Promise>;
+ getRepositories(page?: PageSpec): Promise>
+ getRepositoryUrls(ref: EntityRef): Promise
+}
+
+export interface EntityRef {
+ ref: string;
+ compound: CompoundEntityRef;
+ load(): Promise;
+}
+
+export interface Page {
+ hasNextPage: boolean;
+ hasPreviousPage: boolean;
+ beginCursor: string;
+ endCursor: string;
+ items: {
+ cursor: string;
+ value: T;
+ }[];
+}
+
+export type PageSpec = {
+ count: number;
+ before: string;
+} | {
+ count: number;
+ after: string;
+}
+
+export type GetComponentRef = (name: string) => Promise
diff --git a/plugins/platform/.eslintrc.js b/plugins/platform/.eslintrc.js
new file mode 100644
index 0000000000..86d582272c
--- /dev/null
+++ b/plugins/platform/.eslintrc.js
@@ -0,0 +1,6 @@
+module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, {
+ rules: {
+ 'no-else-return': 'off',
+ 'prefer-const': 'off',
+ }
+});
diff --git a/plugins/platform/README.md b/plugins/platform/README.md
new file mode 100644
index 0000000000..319126d862
--- /dev/null
+++ b/plugins/platform/README.md
@@ -0,0 +1,13 @@
+# platform
+
+Welcome to the platform plugin!
+
+_This plugin was created through the Backstage CLI_
+
+## Getting started
+
+Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/platform](http://localhost:3000/platform).
+
+You can also serve the plugin in isolation by running `yarn start` in the plugin directory.
+This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads.
+It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory.
diff --git a/plugins/platform/dev/index.tsx b/plugins/platform/dev/index.tsx
new file mode 100644
index 0000000000..d708d5f6e6
--- /dev/null
+++ b/plugins/platform/dev/index.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import { createDevApp } from '@backstage/dev-utils';
+import { platformPlugin, PlatformPage } from '../src/plugin';
+
+createDevApp()
+ .registerPlugin(platformPlugin)
+ .addPage({
+ element: ,
+ title: 'Root Page',
+ path: '/platform'
+ })
+ .render();
diff --git a/plugins/platform/package.json b/plugins/platform/package.json
new file mode 100644
index 0000000000..73686506ad
--- /dev/null
+++ b/plugins/platform/package.json
@@ -0,0 +1,54 @@
+{
+ "name": "@frontside/backstage-plugin-platform",
+ "version": "0.1.0",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "license": "Apache-2.0",
+ "private": true,
+ "publishConfig": {
+ "access": "public",
+ "main": "dist/index.esm.js",
+ "types": "dist/index.d.ts"
+ },
+ "backstage": {
+ "role": "frontend-plugin"
+ },
+ "scripts": {
+ "start": "backstage-cli package start",
+ "build": "backstage-cli package build",
+ "lint": "backstage-cli package lint",
+ "test": "backstage-cli package test",
+ "clean": "backstage-cli package clean",
+ "prepack": "backstage-cli package prepack",
+ "postpack": "backstage-cli package postpack"
+ },
+ "dependencies": {
+ "@backstage/core-components": "^0.11.2",
+ "@backstage/core-plugin-api": "^1.0.3",
+ "@backstage/theme": "^0.2.15",
+ "@material-ui/core": "^4.9.13",
+ "@material-ui/icons": "^4.9.1",
+ "@material-ui/lab": "4.0.0-alpha.57",
+ "ansi-to-react": "^6.1.6",
+ "react-use": "^17.2.4"
+ },
+ "peerDependencies": {
+ "react": "^16.13.1 || ^17.0.0"
+ },
+ "devDependencies": {
+ "@backstage/cli": "^0.20.0",
+ "@backstage/core-app-api": "^1.0.3",
+ "@backstage/dev-utils": "^1.0.1",
+ "@backstage/test-utils": "^1.1.1",
+ "@testing-library/jest-dom": "^5.10.1",
+ "@testing-library/react": "^12.1.3",
+ "@testing-library/user-event": "^14.0.0",
+ "@types/jest": "*",
+ "@types/node": "*",
+ "cross-fetch": "^3.1.5",
+ "msw": "^0.42.0"
+ },
+ "files": [
+ "dist"
+ ]
+}
diff --git a/plugins/platform/src/api/executables-api.ts b/plugins/platform/src/api/executables-api.ts
new file mode 100644
index 0000000000..d7fb5200ad
--- /dev/null
+++ b/plugins/platform/src/api/executables-api.ts
@@ -0,0 +1,10 @@
+import type { DownloadInfo } from '@frontside/backstage-plugin-platform-backend';
+import { createApiRef } from '@backstage/core-plugin-api';
+
+export const executablesApiRef = createApiRef({
+ id: 'plugin.platform.executables',
+});
+
+export interface ExecutablesAPI {
+ fetchExecutables(): Promise;
+}
diff --git a/plugins/platform/src/components/AllExecutables/AllExecutables.tsx b/plugins/platform/src/components/AllExecutables/AllExecutables.tsx
new file mode 100644
index 0000000000..262f256d28
--- /dev/null
+++ b/plugins/platform/src/components/AllExecutables/AllExecutables.tsx
@@ -0,0 +1,51 @@
+import React from 'react';
+import { Table, TableColumn, Progress } from '@backstage/core-components';
+import Alert from '@material-ui/lab/Alert';
+import useAsync from 'react-use/lib/useAsync';
+import { useApi } from '@backstage/core-plugin-api';
+
+import type { DownloadInfo } from '@frontside/backstage-plugin-platform-backend';
+import { executablesApiRef } from '../../api/executables-api';
+
+export const DenseTable = ({ info }: { info: DownloadInfo}) => {
+
+ const columns: TableColumn[] = [
+ { title: 'Architecture', field: 'target' },
+ { title: 'Status', field: 'status' },
+ { title: 'URL', field: 'url' },
+ ];
+
+ let { executableName, executables } = info;
+
+ const data = Object.entries(executables).map(([target, executable]) => {
+ return {
+ id: target,
+ target,
+ url: executable.type === 'compiled' ? executable.url : 'N/A',
+ status: executable.type,
+ };
+ });
+
+ return (
+
+ );
+};
+
+export const AllExecutables = () => {
+ let api = useApi(executablesApiRef);
+ const { value, loading, error } = useAsync(api.fetchExecutables, []);
+
+ if (loading) {
+ return ;
+ } else if (error) {
+ return {error.message};
+ } else {
+ return ;
+ }
+
+};
diff --git a/plugins/platform/src/components/AllExecutables/index.ts b/plugins/platform/src/components/AllExecutables/index.ts
new file mode 100644
index 0000000000..f376585c64
--- /dev/null
+++ b/plugins/platform/src/components/AllExecutables/index.ts
@@ -0,0 +1 @@
+export { AllExecutables } from './AllExecutables';
diff --git a/plugins/platform/src/components/Install/Install.tsx b/plugins/platform/src/components/Install/Install.tsx
new file mode 100644
index 0000000000..8f3c23ed68
--- /dev/null
+++ b/plugins/platform/src/components/Install/Install.tsx
@@ -0,0 +1,68 @@
+import React from 'react';
+import { Typography, Grid } from '@material-ui/core';
+import {
+ InfoCard,
+ Header,
+ Page,
+ Content,
+ ContentHeader,
+ HeaderLabel,
+ SupportButton,
+} from '@backstage/core-components';
+import { AllExecutables } from '../AllExecutables';
+import { executablesApiRef } from '../../api/executables-api';
+import { useApi } from '@backstage/core-plugin-api';
+import useAsync from 'react-use/lib/useAsync';
+import Ansi from 'ansi-to-react';
+
+export const useHelp = () => {
+ let api = useApi(executablesApiRef);
+ let info = useAsync(api.fetchExecutables, []);
+ if (info.loading) {
+ return { loading: true };
+ } else if (info.error) {
+ return info;
+ } else if (info.value?.helpText.type === 'rejected' ) {
+ return { loading: false, error: info.value.helpText.error };
+ } else if (info.value?.helpText.type === 'resolved') {
+ return { loading: false, value: info.value?.helpText.value };
+ } else {
+ return { loading: true };
+ }
+}
+
+export const Install = () => {
+ let helpText = useHelp();
+ return (
+
+
+
+
+ Install your company's platform tool
+
+
+
+
+
+ curl -sSL http://localhost:7007/api/idp/install.sh | sh
+
+
+
+
+
+
+ {helpText.error ? "" : helpText.value }
+
+
+
+
+
+
+
+
+
+ )
+};
diff --git a/plugins/platform/src/components/Install/index.ts b/plugins/platform/src/components/Install/index.ts
new file mode 100644
index 0000000000..2c54307650
--- /dev/null
+++ b/plugins/platform/src/components/Install/index.ts
@@ -0,0 +1 @@
+export { Install } from './Install';
diff --git a/plugins/platform/src/index.ts b/plugins/platform/src/index.ts
new file mode 100644
index 0000000000..ead53f1314
--- /dev/null
+++ b/plugins/platform/src/index.ts
@@ -0,0 +1 @@
+export { platformPlugin, PlatformPage } from './plugin';
diff --git a/plugins/platform/src/plugin.test.ts b/plugins/platform/src/plugin.test.ts
new file mode 100644
index 0000000000..ba829a47f1
--- /dev/null
+++ b/plugins/platform/src/plugin.test.ts
@@ -0,0 +1,7 @@
+import { platformPlugin } from './plugin';
+
+describe('platform', () => {
+ it('should export plugin', () => {
+ expect(platformPlugin).toBeDefined();
+ });
+});
diff --git a/plugins/platform/src/plugin.ts b/plugins/platform/src/plugin.ts
new file mode 100644
index 0000000000..1b196b93dc
--- /dev/null
+++ b/plugins/platform/src/plugin.ts
@@ -0,0 +1,34 @@
+import { createApiFactory, createPlugin, discoveryApiRef, createRoutableExtension } from '@backstage/core-plugin-api';
+import { ExecutablesAPI, executablesApiRef } from './api/executables-api'
+import { rootRouteRef } from './routes';
+
+export const platformPlugin = createPlugin({
+ id: 'platform',
+ routes: {
+ root: rootRouteRef,
+ },
+ apis: [
+ createApiFactory({
+ api: executablesApiRef,
+ deps: { discoveryApi: discoveryApiRef },
+ factory({ discoveryApi, }) {
+ return {
+ async fetchExecutables() {
+ let baseUrl = await discoveryApi.getBaseUrl('idp');
+ let response = await fetch(`${baseUrl}/executables`);
+ return await response.json();
+ }
+ } as ExecutablesAPI;
+ }
+ })
+ ]
+});
+
+export const PlatformPage = platformPlugin.provide(
+ createRoutableExtension({
+ name: 'PlatformPage',
+ component: () =>
+ import('./components/Install').then(m => m.Install),
+ mountPoint: rootRouteRef,
+ }),
+);
diff --git a/plugins/platform/src/routes.ts b/plugins/platform/src/routes.ts
new file mode 100644
index 0000000000..d6b3157ddd
--- /dev/null
+++ b/plugins/platform/src/routes.ts
@@ -0,0 +1,5 @@
+import { createRouteRef } from '@backstage/core-plugin-api';
+
+export const rootRouteRef = createRouteRef({
+ id: 'platform',
+});
diff --git a/plugins/platform/src/setupTests.ts b/plugins/platform/src/setupTests.ts
new file mode 100644
index 0000000000..48c09b5346
--- /dev/null
+++ b/plugins/platform/src/setupTests.ts
@@ -0,0 +1,2 @@
+import '@testing-library/jest-dom';
+import 'cross-fetch/polyfill';
diff --git a/tsconfig.json b/tsconfig.json
index fda6cf4884..d6ce168934 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -6,7 +6,10 @@
"plugins/*/dev",
"plugins/*/migrations"
],
- "exclude": ["node_modules", "packages/graphgen"],
+ "exclude": [
+ "node_modules",
+ "packages/graphgen"
+ ],
"compilerOptions": {
"outDir": "dist-types",
"rootDir": "."
diff --git a/yarn.lock b/yarn.lock
index 6c4591b299..423d3c09a7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1932,7 +1932,7 @@
yaml "^2.0.0"
yup "^0.32.9"
-"@backstage/config@^1.0.3":
+"@backstage/config@^1.0.1", "@backstage/config@^1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@backstage/config/-/config-1.0.3.tgz#9b27661c11b010fce974d25519c57fe28004d4a1"
integrity sha512-KFxtGnqTFhN9zS90SLSTcDgJpsSd3IvqfeYLN8IvqfwG2lzaYCRxWRK3XlwOe2PY6Lv98YlMEWfNw5DKw46sgA==
@@ -1940,7 +1940,7 @@
"@backstage/types" "^1.0.0"
lodash "^4.17.21"
-"@backstage/core-app-api@^1.1.1":
+"@backstage/core-app-api@^1.0.3", "@backstage/core-app-api@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@backstage/core-app-api/-/core-app-api-1.1.1.tgz#4217ca4b2e30187f5c8f7d0c74115ba52e6f5282"
integrity sha512-rCR9llaPeB2POEphKUfwEMSOStTdrgyC+qozKdNvfLw0hcK6vewtZL0mhy8gy2gShybEzLvU4lEdPzeAIKDJBg==
@@ -1998,7 +1998,7 @@
zen-observable "^0.8.15"
zod "^3.11.6"
-"@backstage/core-plugin-api@^1.0.7":
+"@backstage/core-plugin-api@^1.0.3", "@backstage/core-plugin-api@^1.0.7":
version "1.0.7"
resolved "https://registry.yarnpkg.com/@backstage/core-plugin-api/-/core-plugin-api-1.0.7.tgz#2c02593bb8c3a2d5aa645780a8b7c2a97744d914"
integrity sha512-bti6TT4CfJPzQXb6p7kJI+NhvaGkiLzcHja/I4JJTOzkKmw6++e0raGDyRt9VybILRCvFgnXHEUkWehRZMu74A==
@@ -2010,7 +2010,7 @@
prop-types "^15.7.2"
zen-observable "^0.8.15"
-"@backstage/dev-utils@^1.0.7":
+"@backstage/dev-utils@^1.0.1", "@backstage/dev-utils@^1.0.7":
version "1.0.7"
resolved "https://registry.yarnpkg.com/@backstage/dev-utils/-/dev-utils-1.0.7.tgz#437ec9dd371f476c4d31b5e72d2f601992e96dfa"
integrity sha512-nzngb8dM/+wyccOhyni0Jh74peFXSxB5V/goB/9DTh5iZeou+5ivA5ys8wxy72rmkTtwkxWVwPBRCQFvh3mRxA==
@@ -2842,7 +2842,7 @@
dependencies:
cross-fetch "^3.1.5"
-"@backstage/test-utils@^1.2.1":
+"@backstage/test-utils@^1.1.1", "@backstage/test-utils@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@backstage/test-utils/-/test-utils-1.2.1.tgz#033366564b44d43b9100a228c4af3c9612f7d00b"
integrity sha512-YE8rOcb1nQ1EFWcChr2Ld4NNMSHKqU2HfnH7E2voPV0ZkebhEEVRa2tl3apaHGvN5hhLvNvt9YWb8vvP3Y65dA==
@@ -2862,7 +2862,7 @@
cross-fetch "^3.1.5"
zen-observable "^0.8.15"
-"@backstage/theme@^0.2.16":
+"@backstage/theme@^0.2.15", "@backstage/theme@^0.2.16":
version "0.2.16"
resolved "https://registry.yarnpkg.com/@backstage/theme/-/theme-0.2.16.tgz#463abce6f55e160a3a61e6654603f20b4f259a9e"
integrity sha512-UDVqQhPunL3uDrhhKP72HlvEoG3rv2dspPxCEGqAAICBjXdLGh7CZ2qGlwdBxDjvCZ/tJcg9GNfpa6SOzdMJmA==
@@ -3269,6 +3269,15 @@
fp-ts "^2.8.2"
monocle-ts "^2.3.3"
+"@effection/channel@2.0.3":
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/@effection/channel/-/channel-2.0.3.tgz#825ade1a4a09b860efdf7077fc02f81d6c7614bb"
+ integrity sha512-HZE2q7dtErIur0g+BVMPqa+dVBgrIIaYMrzMBNM1UoIB6urMGgr+uPWXwgQb3Vzm4Il9SCiCzoM3RE3gDiV1Ig==
+ dependencies:
+ "@effection/core" "2.2.0"
+ "@effection/events" "2.0.3"
+ "@effection/stream" "2.0.3"
+
"@effection/channel@2.0.4":
version "2.0.4"
resolved "https://registry.yarnpkg.com/@effection/channel/-/channel-2.0.4.tgz#f8fcb82d2fe402403b1479d0e3f2d4916da7d4ad"
@@ -3278,6 +3287,13 @@
"@effection/events" "2.0.4"
"@effection/stream" "2.0.4"
+"@effection/core@2.2.0":
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/@effection/core/-/core-2.2.0.tgz#4d11d7948144aecd70a26daf8abaa29ee89bc259"
+ integrity sha512-1RBMrDS0Ya02NEM0TQQRwzlGDSZmwoHhuD3qmWp9NLjZowhO1gJBZ16fQL2NbKvcpS71xho+oZsDedId+C1q8Q==
+ dependencies:
+ abort-controller "^3.0.0"
+
"@effection/core@2.2.1":
version "2.2.1"
resolved "https://registry.yarnpkg.com/@effection/core/-/core-2.2.1.tgz#5115fa14158c10d2d0693f524033fdf356f72083"
@@ -3292,6 +3308,14 @@
dependencies:
effection "2.0.6"
+"@effection/events@2.0.3":
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/@effection/events/-/events-2.0.3.tgz#cf212748f8e433dcf776e5e1dd0145716213a7bc"
+ integrity sha512-x8NBNXHZxI4SJ/db1zy7zs6BRtMIKu8NgymUMpbyrRdapPSIu6rmf4WgXyWrk1uvQPSViEkxOXPw8B2MLu/YnA==
+ dependencies:
+ "@effection/core" "2.2.0"
+ "@effection/stream" "2.0.3"
+
"@effection/events@2.0.4":
version "2.0.4"
resolved "https://registry.yarnpkg.com/@effection/events/-/events-2.0.4.tgz#82173e7e9262ba8746c0c36d37f3f8d9c6d0d2c1"
@@ -3300,6 +3324,14 @@
"@effection/core" "2.2.1"
"@effection/stream" "2.0.4"
+"@effection/fetch@2.0.4":
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/@effection/fetch/-/fetch-2.0.4.tgz#8f76f0b630b3974ef267bd8803599448db956cda"
+ integrity sha512-IhUYqSAM0stEB6VCWK9Mz8F56jWFFpM9yQ4boxGs4F/sdoHnY/9KKW1zxAY05jyYPUHxuUky7sazZVFJm5xRmw==
+ dependencies:
+ "@effection/core" "2.2.0"
+ cross-fetch "3.1.5"
+
"@effection/fetch@2.0.5":
version "2.0.5"
resolved "https://registry.yarnpkg.com/@effection/fetch/-/fetch-2.0.5.tgz#786c73e2c327aed37e1e010691d0090fc06ee5dd"
@@ -3336,6 +3368,15 @@
assert-ts "^0.3.4"
effection "2.0.6"
+"@effection/main@2.1.0":
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/@effection/main/-/main-2.1.0.tgz#90a691d1a78e17ec27ba7ff7e5a87894182c2876"
+ integrity sha512-jnAlVjsLy1feJeNBLHwmA/mDpnoJsYO3gGtAg2XALS4EiIc7nhNDeoj9D6bsBxqUHHEp2FYupYztFK0vU11UFA==
+ dependencies:
+ "@effection/core" "2.2.0"
+ chalk "^4.1.2"
+ stacktrace-parser "^0.1.10"
+
"@effection/main@2.1.1":
version "2.1.1"
resolved "https://registry.yarnpkg.com/@effection/main/-/main-2.1.1.tgz#c47225081130d11092fa92b2fafa14362c1cf307"
@@ -3345,6 +3386,16 @@
chalk "^4.1.2"
stacktrace-parser "^0.1.10"
+"@effection/process@^2.1.1":
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/@effection/process/-/process-2.1.1.tgz#bf48a884faa06b8004c473065c385ab3939e6439"
+ integrity sha512-VNRbRCKwbP48iZcDB66pH4oe77dmffdeQVnLHFth2HIPfJisGH0vTE/3aAQ+1KDaJImWXtq4xOTaurjwqYPHEg==
+ dependencies:
+ cross-spawn "^7.0.3"
+ ctrlc-windows "^2.1.0"
+ effection "2.0.5"
+ shellwords "^0.1.1"
+
"@effection/process@^2.1.2":
version "2.1.2"
resolved "https://registry.yarnpkg.com/@effection/process/-/process-2.1.2.tgz#adc473ad9ed0916df3c34e0bba1f697ac29afb50"
@@ -3362,6 +3413,14 @@
dependencies:
effection "2.0.6"
+"@effection/stream@2.0.3":
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/@effection/stream/-/stream-2.0.3.tgz#c1610c63dfe6d10b0b6aeda6969fea24018364b0"
+ integrity sha512-l1A8PUfxR04eyUBOD1H5gdCu4U5OMwUh2TB/O/IeUlfVoP9tg64daTu7zpZGij9uDcDWV5IP9LJYoWe2lVGbKg==
+ dependencies:
+ "@effection/core" "2.2.0"
+ "@effection/subscription" "2.0.3"
+
"@effection/stream@2.0.4":
version "2.0.4"
resolved "https://registry.yarnpkg.com/@effection/stream/-/stream-2.0.4.tgz#7fec4edaf0db13d604dcd99997745177760b2bdf"
@@ -3370,6 +3429,13 @@
"@effection/core" "2.2.1"
"@effection/subscription" "2.0.4"
+"@effection/subscription@2.0.3":
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/@effection/subscription/-/subscription-2.0.3.tgz#ab6e56bf52663b769eb00d0022195b2e753a127d"
+ integrity sha512-P+bAh0iqCduvzAM+0hbn29HJ+J4TT+lkJTDydw3tI6lSe/OVX9+FJhX/zx2QW3APjFpseMJphjRiEyzf21b/Xw==
+ dependencies:
+ "@effection/core" "2.2.0"
+
"@effection/subscription@2.0.4":
version "2.0.4"
resolved "https://registry.yarnpkg.com/@effection/subscription/-/subscription-2.0.4.tgz#ea4cabc747f4c050311b140b7e558e471ece77bd"
@@ -6933,6 +6999,11 @@
"@types/node" "*"
"@types/responselike" "*"
+"@types/caseless@*":
+ version "0.12.2"
+ resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.2.tgz#f65d3d6389e01eeb458bd54dc8f52b95a9463bc8"
+ integrity sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==
+
"@types/connect-history-api-fallback@^1.3.5":
version "1.3.5"
resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz#d1f7a8a09d0ed5a57aee5ae9c18ab9b803205dae"
@@ -7322,6 +7393,11 @@
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301"
integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==
+"@types/nunjucks@^3.2.1":
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/@types/nunjucks/-/nunjucks-3.2.1.tgz#02a3ade3dc4d3950029c6466a4034565dba7cf8c"
+ integrity sha512-hUh5HIC7peH+0MvlYU5KM2RydWxG1mBceivHsQGwlelU9zlczLICyJmjMwgjkI3m0+N50n46GVHkw35lIim6LQ==
+
"@types/oauth@*":
version "0.9.1"
resolved "https://registry.yarnpkg.com/@types/oauth/-/oauth-0.9.1.tgz#e17221e7f7936b0459ae7d006255dff61adca305"
@@ -7424,6 +7500,16 @@
"@types/scheduler" "*"
csstype "^3.0.2"
+"@types/request@^2.48.8":
+ version "2.48.8"
+ resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.8.tgz#0b90fde3b655ab50976cb8c5ac00faca22f5a82c"
+ integrity sha512-whjk1EDJPcAR2kYHRbFl/lKeeKYTi05A15K9bnLInCVroNDCtXce57xKdI0/rQaA3K+6q0eFyUBPmqfSndUZdQ==
+ dependencies:
+ "@types/caseless" "*"
+ "@types/node" "*"
+ "@types/tough-cookie" "*"
+ form-data "^2.5.0"
+
"@types/resolve@1.17.1":
version "1.17.1"
resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6"
@@ -8000,6 +8086,11 @@ address@^1.0.1, address@^1.1.2:
resolved "https://registry.yarnpkg.com/address/-/address-1.2.0.tgz#d352a62c92fee90f89a693eccd2a8b2139ab02d9"
integrity sha512-tNEZYz5G/zYunxFm7sfhAxkXEuLj3K6BKwv6ZURlsF6yiUQ65z0Q2wZW9L5cPUl9ocofGvXOdFYbFHp0+6MOig==
+adm-zip@^0.5.4:
+ version "0.5.9"
+ resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.5.9.tgz#b33691028333821c0cf95c31374c5462f2905a83"
+ integrity sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg==
+
agent-base@6, agent-base@^6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
@@ -8068,6 +8159,11 @@ alea@1.0.1:
resolved "https://registry.yarnpkg.com/alea/-/alea-1.0.1.tgz#957f60741c5ad11b13f72aa02a6b89fe96a26dc4"
integrity sha512-QU+wv+ziDXaMxRdsQg/aH7sVfWdhKps5YP97IIwFkHCsbDZA3k87JXoZ5/iuemf4ntytzIWeScrRpae8+lDrXA==
+anser@^1.4.1:
+ version "1.4.10"
+ resolved "https://registry.yarnpkg.com/anser/-/anser-1.4.10.tgz#befa3eddf282684bd03b63dcda3927aef8c2e35b"
+ integrity sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==
+
ansi-colors@^4.1.1, ansi-colors@^4.1.3:
version "4.1.3"
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b"
@@ -8124,6 +8220,14 @@ ansi-styles@^5.0.0:
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b"
integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==
+ansi-to-react@^6.1.6:
+ version "6.1.6"
+ resolved "https://registry.yarnpkg.com/ansi-to-react/-/ansi-to-react-6.1.6.tgz#d6fe15ecd4351df626a08121b1646adfe6c02ccb"
+ integrity sha512-+HWn72GKydtupxX9TORBedqOMsJRiKTqaLUKW8txSBZw9iBpzPKLI8KOu4WzwD4R7hSv1zEspobY6LwlWvwZ6Q==
+ dependencies:
+ anser "^1.4.1"
+ escape-carriage "^1.3.0"
+
any-promise@^1.0.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
@@ -9446,7 +9550,7 @@ cli-spinners@^2.5.0:
resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.1.tgz#adc954ebe281c37a6319bfa401e6dd2488ffb70d"
integrity sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==
-cli-table3@~0.6.1:
+cli-table3@^0.6.3, cli-table3@~0.6.1:
version "0.6.3"
resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.3.tgz#61ab765aac156b52f222954ffc607a6f01dbeeb2"
integrity sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==
@@ -10779,6 +10883,14 @@ delegates@^1.0.0:
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==
+deno-bin@^1.26.0:
+ version "1.26.0"
+ resolved "https://registry.yarnpkg.com/deno-bin/-/deno-bin-1.26.0.tgz#03158f4d75950866051e81f7a98d30740b51b958"
+ integrity sha512-4RIh4Igx2F4E4EGttAerFV35QIzyHB9fzw4tTBTEUKHyom/Kuj85WoHDggBvoPliAAd+xDk7fJT9dyLu1w4EpQ==
+ dependencies:
+ adm-zip "^0.5.4"
+ follow-redirects "^1.10.0"
+
denque@^2.0.1:
version "2.1.0"
resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1"
@@ -11119,6 +11231,19 @@ ee-first@1.1.1:
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
+effection@2.0.5, effection@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/effection/-/effection-2.0.5.tgz#caac782994f8f69644bac3eda32228d8799dd244"
+ integrity sha512-q+5iex8LMWP3kitokhkCQErDIN1awRHy7MVqsDIweGeTl0rgpX4Y1KDjE4onvnPF9JtyvhOEFpAAUMRnqi0wqg==
+ dependencies:
+ "@effection/channel" "2.0.3"
+ "@effection/core" "2.2.0"
+ "@effection/events" "2.0.3"
+ "@effection/fetch" "2.0.4"
+ "@effection/main" "2.1.0"
+ "@effection/stream" "2.0.3"
+ "@effection/subscription" "2.0.3"
+
effection@2.0.6, effection@^2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/effection/-/effection-2.0.6.tgz#a2e5f8dc3ac0751882bb595f73ccb14c7bdc09b6"
@@ -11623,6 +11748,11 @@ escalade@^3.1.1:
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
+escape-carriage@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/escape-carriage/-/escape-carriage-1.3.0.tgz#71006b2d4da8cb6828686addafcb094239c742f3"
+ integrity sha512-ATWi5MD8QlAGQOeMgI8zTp671BG8aKvAC0M7yenlxU4CRLGO/sKthxVUyjiOFKjHdIo+6dZZUNFgHFeVEaKfGQ==
+
escape-html@^1.0.3, escape-html@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
@@ -12477,7 +12607,7 @@ follow-redirects@^1.0.0, follow-redirects@^1.14.0:
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5"
integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==
-follow-redirects@^1.14.9:
+follow-redirects@^1.10.0, follow-redirects@^1.14.9:
version "1.15.2"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
@@ -12541,7 +12671,7 @@ form-data-encoder@^1.4.3, form-data-encoder@^1.7.1:
resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-1.7.2.tgz#1f1ae3dccf58ed4690b86d87e4f57c654fbab040"
integrity sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==
-form-data@^2.3.1, form-data@^2.3.2:
+form-data@^2.3.1, form-data@^2.3.2, form-data@^2.5.0:
version "2.5.1"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4"
integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==
@@ -17495,11 +17625,25 @@ node-cache@^5.1.2:
dependencies:
clone "2.x"
+node-deno@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/node-deno/-/node-deno-0.1.0.tgz#b70a9307d93db03bca69da0377ad3b396f4486e0"
+ integrity sha512-RPwAQZQHaadrejl83mDOq0LbNgefveH+QYOT1r5IUKmmqc/hKizNSt0xlWEDCy+Hvb5pCurk75FGK5XFb3eD0g==
+ dependencies:
+ "@effection/process" "^2.1.1"
+ deno-bin "^1.26.0"
+ effection "^2.0.5"
+
node-domexception@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5"
integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==
+node-fetch-native@^0.1.7:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-0.1.7.tgz#8a8ed0d5d1d1b89d34c6731a9d69d407c09df067"
+ integrity sha512-hps7dFJM0IEF056JftDSSjWDAwW9v2clwHoUJiHyYgl+ojoqjKyWybljMlpTmlC1O+864qovNlRLyAIjRxu9Ag==
+
node-fetch@2.6.7, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.7:
version "2.6.7"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"