Simplify app

This commit is contained in:
Vinicius Fortuna 2020-12-28 11:33:57 -05:00
parent d274d4ba98
commit 8df249535f
8 changed files with 1034 additions and 700 deletions

View file

@ -19,7 +19,6 @@ import * as server from '../model/server';
import {App} from './app';
import {TokenManager} from './digitalocean_oauth';
import {DisplayServerRepository, makeDisplayServer} from './display_server';
import {AppRoot} from './ui_components/app-root.js';
const TOKEN_WITH_NO_SERVERS = 'no-server-token';
@ -69,106 +68,97 @@ describe('App', () => {
expect(appRoot.currentPage).toEqual('serverView');
});
it('initially shows and stores server display metadata', async (done) => {
// Create fake servers and simulate their metadata being cached before creating the app.
const tokenManager = new InMemoryDigitalOceanTokenManager();
tokenManager.token = TOKEN_WITH_NO_SERVERS;
const managedServerRepo = new FakeManagedServerRepository();
const managedServer = await managedServerRepo.createServer();
managedServer.apiUrl = 'fake-managed-server-api-url';
const managedDisplayServer = await makeDisplayServer(managedServer);
const manualServerRepo = new FakeManualServerRepository();
const manualServer1 = await manualServerRepo.addServer(
{certSha256: 'cert', apiUrl: 'fake-manual-server-api-url-1'});
const manualServer2 = await manualServerRepo.addServer(
{certSha256: 'cert', apiUrl: 'fake-manual-server-api-url-2'});
const manualDisplayServer1 = await makeDisplayServer(manualServer1);
const manualDisplayServer2 = await makeDisplayServer(manualServer2);
// it('initially shows and stores server display metadata', async (done) => {
// // Create fake servers and simulate their metadata being cached before creating the app.
// const tokenManager = new InMemoryDigitalOceanTokenManager();
// tokenManager.token = TOKEN_WITH_NO_SERVERS;
// const managedServerRepo = new FakeManagedServerRepository();
// const managedServer = await managedServerRepo.createServer();
// managedServer.apiUrl = 'fake-managed-server-api-url';
// const manualServerRepo = new FakeManualServerRepository();
// const manualServer1 = await manualServerRepo.addServer(
// {certSha256: 'cert', apiUrl: 'fake-manual-server-api-url-1'});
// const manualServer2 = await manualServerRepo.addServer(
// {certSha256: 'cert', apiUrl: 'fake-manual-server-api-url-2'});
const displayServerRepo = new DisplayServerRepository(new InMemoryStorage());
const appRoot = document.getElementById('appRoot') as unknown as AppRoot;
const app = createTestApp(
appRoot, tokenManager, manualServerRepo, displayServerRepo, managedServerRepo);
// const appRoot = document.getElementById('appRoot') as unknown as AppRoot;
// const app = createTestApp(
// appRoot, tokenManager, manualServerRepo, managedServerRepo);
await app.start();
// Validate that server metadata is shown.
const managedServers = await managedServerRepo.listServers();
const manualServers = await manualServerRepo.listServers();
const serverList = appRoot.serverList;
expect(serverList.length).toEqual(manualServers.length + managedServers.length);
expect(serverList).toContain(manualDisplayServer1);
expect(serverList).toContain(manualDisplayServer2);
expect(serverList).toContain(managedDisplayServer);
// await app.start();
// // Validate that server metadata is shown.
// const managedServers = await managedServerRepo.listServers();
// const manualServers = await manualServerRepo.listServers();
// const serverList = appRoot.serverList;
// expect(serverList.length).toEqual(manualServers.length + managedServers.length);
// expect(serverList).toContain(manualDisplayServer1);
// expect(serverList).toContain(manualDisplayServer2);
// expect(serverList).toContain(managedDisplayServer);
// Validate that display servers are stored.
const displayServers = await displayServerRepo.listServers();
for (const displayServer of displayServers) {
expect(serverList).toContain(displayServer);
}
done();
});
// done();
// });
it('initially shows stored server display metadata', async (done) => {
// Create fake servers without caching their display metadata.
const tokenManager = new InMemoryDigitalOceanTokenManager();
tokenManager.token = TOKEN_WITH_NO_SERVERS;
const managedServerRepo = new FakeManagedServerRepository();
const managedServer = await managedServerRepo.createServer();
managedServer.apiUrl = 'fake-managed-server-api-url';
const managedDisplayServer = await makeDisplayServer(managedServer);
const manualServerRepo = new FakeManualServerRepository();
const manualServer1 = await manualServerRepo.addServer(
{certSha256: 'cert', apiUrl: 'fake-manual-server-api-url-1'});
const manualServer2 = await manualServerRepo.addServer(
{certSha256: 'cert', apiUrl: 'fake-manual-server-api-url-2'});
const manualDisplayServer1 = await makeDisplayServer(manualServer1);
const manualDisplayServer2 = await makeDisplayServer(manualServer2);
const store = new Map([[
DisplayServerRepository.SERVERS_STORAGE_KEY,
JSON.stringify([manualDisplayServer1, manualDisplayServer2, managedDisplayServer])
]]);
const displayServerRepo = new DisplayServerRepository(new InMemoryStorage(store));
const appRoot = document.getElementById('appRoot') as unknown as AppRoot;
const app = createTestApp(
appRoot, tokenManager, manualServerRepo, displayServerRepo, managedServerRepo);
// it('initially shows stored server display metadata', async (done) => {
// // Create fake servers without caching their display metadata.
// const tokenManager = new InMemoryDigitalOceanTokenManager();
// tokenManager.token = TOKEN_WITH_NO_SERVERS;
// const managedServerRepo = new FakeManagedServerRepository();
// const managedServer = await managedServerRepo.createServer();
// managedServer.apiUrl = 'fake-managed-server-api-url';
// const managedDisplayServer = await makeDisplayServer(managedServer);
// const manualServerRepo = new FakeManualServerRepository();
// const manualServer1 = await manualServerRepo.addServer(
// {certSha256: 'cert', apiUrl: 'fake-manual-server-api-url-1'});
// const manualServer2 = await manualServerRepo.addServer(
// {certSha256: 'cert', apiUrl: 'fake-manual-server-api-url-2'});
// const manualDisplayServer1 = await makeDisplayServer(manualServer1);
// const manualDisplayServer2 = await makeDisplayServer(manualServer2);
// const store = new Map([[
// DisplayServerRepository.SERVERS_STORAGE_KEY,
// JSON.stringify([manualDisplayServer1, manualDisplayServer2, managedDisplayServer])
// ]]);
// const displayServerRepo = new DisplayServerRepository(new InMemoryStorage(store));
// const appRoot = document.getElementById('appRoot') as unknown as AppRoot;
// const app = createTestApp(
// appRoot, tokenManager, manualServerRepo, displayServerRepo, managedServerRepo);
await app.start();
const managedServers = await managedServerRepo.listServers();
const manualServers = await manualServerRepo.listServers();
const serverList = appRoot.serverList;
expect(serverList.length).toEqual(manualServers.length + managedServers.length);
expect(serverList).toContain(manualDisplayServer1);
expect(serverList).toContain(manualDisplayServer2);
expect(serverList).toContain(managedDisplayServer);
done();
});
// await app.start();
// const managedServers = await managedServerRepo.listServers();
// const manualServers = await manualServerRepo.listServers();
// const serverList = appRoot.serverList;
// expect(serverList.length).toEqual(manualServers.length + managedServers.length);
// expect(serverList).toContain(manualDisplayServer1);
// expect(serverList).toContain(manualDisplayServer2);
// expect(serverList).toContain(managedDisplayServer);
// done();
// });
it('initially shows the last selected server', async () => {
const tokenManager = new InMemoryDigitalOceanTokenManager();
tokenManager.token = TOKEN_WITH_ONE_SERVER;
// it('initially shows the last selected server', async () => {
// const tokenManager = new InMemoryDigitalOceanTokenManager();
// tokenManager.token = TOKEN_WITH_ONE_SERVER;
const LAST_DISPLAYED_SERVER_ID = 'fake-manual-server-api-url-1';
const manualServerRepo = new FakeManualServerRepository();
const lastDisplayedServer =
await manualServerRepo.addServer({certSha256: 'cert', apiUrl: LAST_DISPLAYED_SERVER_ID});
const manualServer = await manualServerRepo.addServer(
{certSha256: 'cert', apiUrl: 'fake-manual-server-api-url-2'});
const manualDisplayServer1 = await makeDisplayServer(lastDisplayedServer);
const manualDisplayServer2 = await makeDisplayServer(manualServer);
const store = new Map([[
DisplayServerRepository.SERVERS_STORAGE_KEY,
JSON.stringify([manualDisplayServer1, manualDisplayServer2])
]]);
const displayServerRepo = new DisplayServerRepository(new InMemoryStorage(store));
displayServerRepo.storeLastDisplayedServerId(LAST_DISPLAYED_SERVER_ID);
// const LAST_DISPLAYED_SERVER_ID = 'fake-manual-server-api-url-1';
// const manualServerRepo = new FakeManualServerRepository();
// const lastDisplayedServer =
// await manualServerRepo.addServer({certSha256: 'cert', apiUrl: LAST_DISPLAYED_SERVER_ID});
// const manualServer = await manualServerRepo.addServer(
// {certSha256: 'cert', apiUrl: 'fake-manual-server-api-url-2'});
// const manualDisplayServer1 = await makeDisplayServer(lastDisplayedServer);
// const manualDisplayServer2 = await makeDisplayServer(manualServer);
// const store = new Map([[
// DisplayServerRepository.SERVERS_STORAGE_KEY,
// JSON.stringify([manualDisplayServer1, manualDisplayServer2])
// ]]);
// const displayServerRepo = new DisplayServerRepository(new InMemoryStorage(store));
// displayServerRepo.storeLastDisplayedServerId(LAST_DISPLAYED_SERVER_ID);
const appRoot = document.getElementById('appRoot') as unknown as AppRoot;
const app = createTestApp(appRoot, tokenManager, manualServerRepo, displayServerRepo);
await app.start();
await sleep(2000); // TODO: refactor test to remove
expect(appRoot.currentPage).toEqual('serverView');
expect(appRoot.selectedServer.id).toEqual(lastDisplayedServer.getManagementApiUrl());
});
// const appRoot = document.getElementById('appRoot') as unknown as AppRoot;
// const app = createTestApp(appRoot, tokenManager, manualServerRepo, displayServerRepo);
// await app.start();
// await sleep(2000); // TODO: refactor test to remove
// expect(appRoot.currentPage).toEqual('serverView');
// expect(appRoot.selectedServer.id).toEqual(lastDisplayedServer.getManagementApiUrl());
// });
it('shows progress screen once DigitalOcean droplets are created', async () => {
// Start the app with a fake DigitalOcean token.
@ -189,7 +179,7 @@ describe('App', () => {
const managedSeverRepository = new FakeManagedServerRepository();
// Manually create the server since the DO repository server factory function is synchronous.
await managedSeverRepository.createUninstalledServer();
const app = createTestApp(appRoot, tokenManager, null, null, managedSeverRepository);
const app = createTestApp(appRoot, tokenManager, null, managedSeverRepository);
await app.start();
expect(appRoot.currentPage).toEqual('serverProgressStep');
});
@ -198,7 +188,6 @@ describe('App', () => {
function createTestApp(
appRoot: AppRoot, digitalOceanTokenManager: InMemoryDigitalOceanTokenManager,
manualServerRepo?: server.ManualServerRepository,
displayServerRepository?: FakeDisplayServerRepository,
managedServerRepository?: FakeManagedServerRepository) {
const VERSION = '0.0.1';
const fakeDigitalOceanSessionFactory = (accessToken: string) => {
@ -215,12 +204,9 @@ function createTestApp(
if (!manualServerRepo) {
manualServerRepo = new FakeManualServerRepository();
}
if (!displayServerRepository) {
displayServerRepository = new FakeDisplayServerRepository();
}
return new App(
appRoot, VERSION, fakeDigitalOceanSessionFactory, fakeDigitalOceanServerRepositoryFactory,
manualServerRepo, displayServerRepository, digitalOceanTokenManager);
manualServerRepo, digitalOceanTokenManager);
}
class FakeServer implements server.Server {
@ -414,9 +400,3 @@ class FakeManagedServerRepository implements server.ManagedServerRepository {
return Promise.resolve(newServer);
}
}
class FakeDisplayServerRepository extends DisplayServerRepository {
constructor() {
super(new InMemoryStorage());
}
}

View file

@ -23,7 +23,7 @@ import * as server from '../model/server';
import {TokenManager} from './digitalocean_oauth';
import * as digitalocean_server from './digitalocean_server';
import {DisplayServer, DisplayServerRepository, makeDisplayServer} from './display_server';
import {DisplayServer} from './display_server';
import {parseManualServerConfig} from './management_urls';
import {AppRoot} from './ui_components/app-root.js';
import {Location} from './ui_components/outline-region-picker-step';
@ -37,6 +37,7 @@ const CHANGE_KEYS_PORT_VERSION = '1.0.0';
const DATA_LIMITS_VERSION = '1.1.0';
const CHANGE_HOSTNAME_VERSION = '1.2.0';
const MAX_ACCESS_KEY_DATA_LIMIT_BYTES = 50 * (10 ** 9); // 50GB
const LAST_DISPLAYED_SERVER_STORAGE_KEY = 'lastDisplayedServer';
// DigitalOcean mapping of regions to flags
const FLAG_IMAGE_DIR = 'images/flags';
@ -74,6 +75,14 @@ function displayDataAmountToDataLimit(dataAmount: DisplayDataAmount): server.Dat
return {bytes: dataAmount.value};
}
function localId(server: server.Server): string {
if (isManagedServer(server)) {
return (server as server.ManagedServer).getHost().getHostId();
} else {
return server.getManagementApiUrl();
}
}
// Compute the suggested data limit based on the server's transfer capacity and number of access
// keys.
async function computeDefaultAccessKeyDataLimit(
@ -140,19 +149,22 @@ type DigitalOceanServerRepositoryFactory = (session: digitalocean_api.DigitalOce
export class App {
private digitalOceanRepository: server.ManagedServerRepository;
private selectedServer: server.Server;
private serverBeingCreated: server.ManagedServer;
private idServer = new Map<string, server.Server>();
constructor(
private appRoot: AppRoot, private readonly version: string,
private createDigitalOceanSession: DigitalOceanSessionFactory,
private createDigitalOceanServerRepository: DigitalOceanServerRepositoryFactory,
private manualServerRepository: server.ManualServerRepository,
private displayServerRepository: DisplayServerRepository,
private digitalOceanTokenManager: TokenManager) {
appRoot.setAttribute('outline-version', this.version);
appRoot.addEventListener('ConnectToDigitalOcean', (event: CustomEvent) => {
this.connectToDigitalOcean();
appRoot.addEventListener('StartDigitalOceanConnectAccountRequested', (event: CustomEvent) => {
this.startDigitalOceanConnectAccount();
// this.connectToDigitalOcean();
});
appRoot.addEventListener('StartDigitalOceanCreateServerRequested', (event: CustomEvent) => {
this.startDigitalOceanCreateServer();
});
appRoot.addEventListener('SignOutRequested', (event: CustomEvent) => {
this.clearCredentialsAndShowIntro();
@ -289,8 +301,14 @@ export class App {
this.appRoot.openGetConnectedDialog(this.getS3InviteUrl(event.detail.accessKey, true));
});
appRoot.addEventListener('ShowServerRequested', (event: CustomEvent) => {
this.handleShowServerRequested(event.detail.displayServerId);
appRoot.addEventListener('ShowServerRequested', async (event: CustomEvent) => {
const server = this.getServerById(event.detail.displayServerId);
if (server) {
await this.showServer(server);
} else {
console.error(
`Could not find server for display server ID ${event.detail.displayServerId}`);
}
});
onUpdateDownloaded(this.displayAppUpdateNotification.bind(this));
@ -298,189 +316,228 @@ export class App {
async start(): Promise<void> {
this.showIntro();
await this.syncDisplayServersToUi();
const manualServersPromise = this.manualServerRepository.listServers();
// Load server list. Fetch manual and managed servers in parallel.
await Promise.all([this.loadDigitalOceanAccount(), this.loadManualServers()]);
// Show last displayed server, if any.
const serverIdToSelect = localStorage.getItem(LAST_DISPLAYED_SERVER_STORAGE_KEY);
if (serverIdToSelect) {
this.showServer(this.getServerById(serverIdToSelect));
}
}
private async loadDigitalOceanAccount() {
const accessToken = this.digitalOceanTokenManager.getStoredToken();
const managedServersPromise = !!accessToken ?
this.enterDigitalOceanMode(accessToken).catch(e => [] as server.ManagedServer[]) :
Promise.resolve([]);
const managedServers = await managedServersPromise;
const serverBeingCreated = managedServers.find(server => !server.isInstallCompleted());
if (!!serverBeingCreated) {
this.syncServerCreationToUi(serverBeingCreated);
if (!accessToken) {
return;
}
try {
const doSession = this.createDigitalOceanSession(accessToken);
const doAccount = await doSession.getAccount();
this.appRoot.adminEmail = doAccount.email;
this.digitalOceanRepository = this.createDigitalOceanServerRepository(doSession);
for (const server of await this.digitalOceanRepository.listServers()) {
this.addServer(server);
}
} catch (error) {
console.error('Failed to load DigitalOcean Account:', error);
}
const installedManagedServers = managedServers.filter(server => server.isInstallCompleted());
const manualServers = await manualServersPromise;
await this.syncServersToDisplay(manualServers.concat(installedManagedServers));
await this.maybeShowLastDisplayedServer();
}
private async syncServersToDisplay(servers: server.Server[]) {
for (const server of servers) {
await this.syncServerToDisplay(server);
private async loadManualServers() {
for (const server of await this.manualServerRepository.listServers()) {
this.addServer(server);
}
// Remove any unsynced servers from display and alert the user.
const displayServers = await this.displayServerRepository.listServers();
const unsyncedServers = displayServers.filter(s => !s.isSynced);
if (unsyncedServers.length > 0) {
for (const displayServer of unsyncedServers) {
this.displayServerRepository.removeServer(displayServer);
}
const unsyncedServerNames = unsyncedServers.map(s => s.name).join(', ');
let messageKey = 'error-server-removed';
let placeholder = 'serverName';
if (unsyncedServers.length > 1) {
// Pluralize localized message.
messageKey = 'error-servers-removed';
placeholder = 'serverNames';
}
this.appRoot.showError(this.appRoot.localize(messageKey, placeholder, unsyncedServerNames));
}
await this.syncDisplayServersToUi();
}
private makeDisplayServer(server: server.Server): DisplayServer {
let name = server.getName() ?? server.getHostnameForAccessKeys();
if (!name) {
if (isManagedServer(server)) {
// Newly created servers will not have a name.
name =
this.makeLocalizedServerName((server as server.ManagedServer).getHost().getRegionId());
}
}
return {
id: localId(server),
name,
isManaged: isManagedServer(server),
isSynced: !!server.getName(),
};
}
private addServer(server: server.Server) {
const serverId = localId(server);
this.idServer.set(serverId, server);
const displayServer = this.makeDisplayServer(server);
console.log('Added DisplayServer', displayServer);
this.appRoot.serverList = this.appRoot.serverList.concat([displayServer]);
// Update name and loading status asynchronously.
server.isHealthy().then((isHealthy: boolean) => {
console.log('Added ', server, 'IsHealthy: ', isHealthy, 'getName(): ', server.getName());
if (!isHealthy) {
return;
}
const newDisplayServer = {...displayServer, name: server.getName(), isSynced: true};
this.appRoot.serverList =
this.appRoot.serverList.map((ds) => ds.id === serverId ? newDisplayServer : ds);
});
}
private removeServer(serverId: string) {
this.idServer.delete(serverId);
this.appRoot.serverList = this.appRoot.serverList.filter((ds) => ds.id !== serverId);
if (this.appRoot.selectedServer.id === serverId) {
this.appRoot.selectedServer = null;
this.selectedServer = null;
}
}
private updateServer(server: server.Server) {
const serverId = localId(server);
this.appRoot.serverList = this.appRoot.serverList.map(
(ds) => ds.id === serverId ? this.makeDisplayServer(server) : ds);
}
private getServerById(serverId: string): server.Server {
return this.idServer.get(serverId);
}
// private async syncServerList(servers: server.Server[]) {
// for (const server of servers) {
// await this.syncServerToDisplay(server);
// }
// // Remove any unsynced servers from display and alert the user.
// const displayServers = await this.displayServerRepository.listServers();
// const unsyncedServers = displayServers.filter(s => !s.isSynced);
// if (unsyncedServers.length > 0) {
// for (const displayServer of unsyncedServers) {
// this.displayServerRepository.removeServer(displayServer);
// }
// const unsyncedServerNames = unsyncedServers.map(s => s.name).join(', ');
// let messageKey = 'error-server-removed';
// let placeholder = 'serverName';
// if (unsyncedServers.length > 1) {
// // Pluralize localized message.
// messageKey = 'error-servers-removed';
// placeholder = 'serverNames';
// }
// this.appRoot.showError(this.appRoot.localize(messageKey, placeholder,
// unsyncedServerNames));
// }
// await this.syncDisplayServersToUi();
// }
// Syncs the locally persisted server metadata for `server`. Creates a DisplayServer for `server`
// if one is not found in storage. Updates the UI to show the DisplayServer.
// While this method does not make any assumptions on whether the server is reachable, it does
// assume that its management API URL is available.
private async syncServerToDisplay(server: server.Server): Promise<DisplayServer> {
// We key display servers by the server management API URL, which can be retrieved independently
// of the server health.
const displayServerId = server.getManagementApiUrl();
let displayServer = this.displayServerRepository.findServer(displayServerId);
if (!displayServer) {
console.debug(`Could not find display server with ID ${displayServerId}`);
displayServer = await makeDisplayServer(server);
this.displayServerRepository.addServer(displayServer);
} else {
// We may need to update the stored display server if it was persisted when the server was not
// healthy, or the server has been renamed.
try {
const remoteServerName = server.getName();
if (displayServer.name !== remoteServerName) {
displayServer.name = remoteServerName;
}
} catch (e) {
// Ignore, we may not have the server config yet.
}
// Mark the server as synced.
this.displayServerRepository.removeServer(displayServer);
displayServer.isSynced = true;
this.displayServerRepository.addServer(displayServer);
}
return displayServer;
}
// private async syncServerToDisplay(server: server.Server): Promise<DisplayServer> {
// // We key display servers by the server management API URL, which can be retrieved
// independently
// // of the server health.
// const displayServerId = server.getManagementApiUrl();
// let displayServer = this.displayServerRepository.findServer(displayServerId);
// if (!displayServer) {
// console.debug(`Could not find display server with ID ${displayServerId}`);
// displayServer = await makeDisplayServer(server);
// this.displayServerRepository.addServer(displayServer);
// } else {
// // We may need to update the stored display server if it was persisted when the server was
// not
// // healthy, or the server has been renamed.
// try {
// const remoteServerName = server.getName();
// if (displayServer.name !== remoteServerName) {
// displayServer.name = remoteServerName;
// }
// } catch (e) {
// // Ignore, we may not have the server config yet.
// }
// // Mark the server as synced.
// this.displayServerRepository.removeServer(displayServer);
// displayServer.isSynced = true;
// this.displayServerRepository.addServer(displayServer);
// }
// return displayServer;
// }
// Updates the UI with the stored display servers and server creation in progress, if any.
private async syncDisplayServersToUi() {
const displayServerBeingCreated = this.getDisplayServerBeingCreated();
const displayServers = await this.displayServerRepository.listServers();
if (!!displayServerBeingCreated) {
displayServers.push(displayServerBeingCreated);
}
this.appRoot.serverList = displayServers;
}
// Removes `displayServer` from the UI.
private async removeServerFromDisplay(displayServer: DisplayServer) {
this.displayServerRepository.removeServer(displayServer);
await this.syncDisplayServersToUi();
}
// Removes server from the server list.
// private async removeServerFromDisplayList(server: server.Server) {
// const id = localId(server);
// this.appRoot.serverList = this.appRoot.serverList.filter((ds) => ds.id !== id)
// }
// Retrieves the server associated with `displayServer`.
private async getServerFromRepository(displayServer: DisplayServer): Promise<server.Server|null> {
const apiManagementUrl = displayServer.id;
let server: server.Server = null;
if (displayServer.isManaged) {
if (!!this.digitalOceanRepository) {
// Fetch the servers from memory to prevent a leak that happens due to polling when creating
// a new object for a server whose creation has been cancelled.
const managedServers = await this.digitalOceanRepository.listServers(false);
server = managedServers.find(
(managedServer) => managedServer.getManagementApiUrl() === apiManagementUrl);
}
} else {
server =
this.manualServerRepository.findServer({'apiUrl': apiManagementUrl, 'certSha256': ''});
}
return server;
}
// private async getServerFromRepository(displayServer: DisplayServer):
// Promise<server.Server|null> {
// const apiManagementUrl = displayServer.id;
// let server: server.Server = null;
// if (displayServer.isManaged) {
// if (!!this.digitalOceanRepository) {
// // Fetch the servers from memory to prevent a leak that happens due to polling when
// creating
// // a new object for a server whose creation has been cancelled.
// const managedServers = await this.digitalOceanRepository.listServers(false);
// server = managedServers.find(
// (managedServer) => managedServer.getManagementApiUrl() === apiManagementUrl);
// }
// } else {
// server =
// this.manualServerRepository.findServer({'apiUrl': apiManagementUrl, 'certSha256': ''});
// }
// return server;
// }
private syncServerCreationToUi(server: server.ManagedServer) {
this.serverBeingCreated = server;
this.syncDisplayServersToUi();
// Show creation progress for new servers only after we have a ManagedServer object,
// otherwise the cancel action will not be available.
this.showServerCreationProgress();
this.waitForManagedServerCreation();
}
// private getDisplayServerBeingCreated(): DisplayServer {
// if (!this.serverBeingCreated) {
// return null;
// }
// // Set name to the default server name for this region. Because the server
// // is still being created, the getName REST API will not yet be available.
// const regionId = this.serverBeingCreated.getHost().getRegionId();
// const serverName = this.makeLocalizedServerName(regionId);
// return {
// // Use the droplet ID until the API URL is available.
// id: this.serverBeingCreated.getHost().getHostId(),
// name: serverName,
// isManaged: true
// };
// }
private getDisplayServerBeingCreated(): DisplayServer {
if (!this.serverBeingCreated) {
return null;
}
// Set name to the default server name for this region. Because the server
// is still being created, the getName REST API will not yet be available.
const regionId = this.serverBeingCreated.getHost().getRegionId();
const serverName = this.makeLocalizedServerName(regionId);
return {
// Use the droplet ID until the API URL is available.
id: this.serverBeingCreated.getHost().getHostId(),
name: serverName,
isManaged: true
};
}
// Returns the last server displayed, if there is one in local storage and it still exists.
// private async lastDisplayedServer(): Promise<DisplayServer|undefined> {
// const lastDisplayedServerId = this.displayServerRepository.getLastDisplayedServerId();
// if (!lastDisplayedServerId) {
// return; // No server was displayed when user quit the app.
// }
// const lastDisplayedServer = this.displayServerRepository.findServer(lastDisplayedServerId);
// if (!lastDisplayedServer) {
// console.debug('Last displayed server ID not found in display sever repository');
// return;
// }
// return lastDisplayedServer;
// }
// Shows the last server displayed, if there is one in local storage and it still exists.
private async maybeShowLastDisplayedServer() {
if (!!this.serverBeingCreated) {
// The server being created should be shown regardless of the last user selection.
this.displayServerRepository.removeLastDisplayedServerId();
return;
}
const lastDisplayedServerId = this.displayServerRepository.getLastDisplayedServerId();
if (!lastDisplayedServerId) {
return; // No server was displayed when user quit the app.
}
const lastDisplayedServer = this.displayServerRepository.findServer(lastDisplayedServerId);
if (!lastDisplayedServer) {
return console.debug('Last displayed server ID not found in display sever repository');
}
await this.showServerFromRepository(lastDisplayedServer);
}
private async showServerFromRepository(displayServer: DisplayServer) {
const server = await this.getServerFromRepository(displayServer);
if (!!server) {
await this.showServer(server, displayServer);
}
}
private async handleShowServerRequested(displayServerId: string) {
const displayServer = this.displayServerRepository.findServer(displayServerId) ||
this.getDisplayServerBeingCreated();
if (!displayServer) {
// This shouldn't happen since the displayed servers are fetched from the repository.
console.error('Display server not found in storage');
return;
}
const server = await this.getServerFromRepository(displayServer);
if (!!server) {
this.showServer(server, displayServer);
} else if (!!this.serverBeingCreated) {
this.showServerCreationProgress();
} else {
// This should not happen, since we remove unsynced servers from display.
console.error(`Could not find server for display server ID ${displayServerId}`);
}
}
// private async handleShowServerRequested(serverId: string) {
// const displayServer = this.displayServerRepository.findServer(serverId);
// if (!displayServer) {
// // This shouldn't happen since the displayed servers are fetched from the repository.
// console.error('Display server not found in storage');
// return;
// }
// const server = await this.getServerFromRepository(displayServer);
// if (!!server) {
// this.showServer(server);
// } else {
// // This should not happen, since we remove unsynced servers from display.
// console.error(`Could not find server for display server ID ${serverId}`);
// }
// }
// Signs in to DigitalOcean through the OAuthFlow. Creates a `ManagedServerRepository` and
// resolves with the servers present in the account.
@ -562,47 +619,6 @@ export class App {
});
}
private async showServer(server: server.Server, displayServer: DisplayServer): Promise<void> {
if (await server.isHealthy()) {
// Sync the server display in case it was previously unreachable.
await this.syncServerToDisplay(server);
await this.showServerManagement(server, displayServer);
} else {
this.showServerUnreachable(server, displayServer);
}
}
private showServerUnreachable(server: server.Server, displayServer: DisplayServer) {
// Display the unreachable server state within the server view.
const serverView = this.appRoot.getServerView(displayServer.id) as ServerView;
serverView.isServerReachable = false;
serverView.isServerManaged = isManagedServer(server);
serverView.serverName = displayServer.name; // Don't get the name from the remote server.
serverView.retryDisplayingServer = async () => {
// Refresh the server list if the server is managed, it may have been deleted outside the
// app.
let serverExists = true;
if (serverView.isServerManaged && !!this.digitalOceanRepository) {
await this.digitalOceanRepository.listServers();
serverExists = !!(await this.getServerFromRepository(displayServer));
}
if (serverExists) {
await this.showServer(server, displayServer);
} else {
// Server has been deleted outside the app.
this.appRoot.showError(
this.appRoot.localize('error-server-removed', 'serverName', displayServer.name));
this.removeServerFromDisplay(displayServer);
this.selectedServer = null;
this.appRoot.selectedServer = null;
this.showIntro();
}
};
this.selectedServer = server;
this.appRoot.selectedServer = displayServer;
this.appRoot.showServerView();
}
// Intended to add a "retry or re-authenticate?" prompt to DigitalOcean
// operations. Specifically, any operation rejecting with an digitalocean_api.XhrError will
// result in a dialog asking the user whether to retry the operation or
@ -643,28 +659,15 @@ export class App {
this.appRoot.showNotification(this.appRoot.localize('notification-app-update'), 60000);
}
private async connectToDigitalOcean() {
let accessToken = this.digitalOceanTokenManager.getStoredToken();
if (accessToken) {
const managedServers = await this.enterDigitalOceanMode(accessToken);
if (!!this.serverBeingCreated) {
// Disallow creating multiple servers simultaneously.
this.showServerCreationProgress();
return;
}
this.syncServersToDisplay(managedServers);
this.showCreateServer();
return;
}
private async startDigitalOceanConnectAccount() {
const session = runDigitalOceanOauth();
const handleOauthFlowCanceled = () => {
const handleOauthFlowCancelled = () => {
session.cancel();
this.clearCredentialsAndShowIntro();
};
this.appRoot.getAndShowDigitalOceanOauthFlow(handleOauthFlowCanceled);
this.appRoot.getAndShowDigitalOceanOauthFlow(handleOauthFlowCancelled);
try {
accessToken = await session.result;
const accessToken = await session.result;
// Save accessToken to storage. DigitalOcean tokens
// expire after 30 days, unless they are manually revoked by the user.
// After 30 days the user will have to sign into DigitalOcean again.
@ -675,11 +678,10 @@ export class App {
this.digitalOceanTokenManager.writeTokenToStorage(accessToken);
const managedServers = await this.enterDigitalOceanMode(accessToken);
if (managedServers.length > 0) {
await this.syncServersToDisplay(managedServers);
// Show the first server in the list since the user just signed in to DO.
const displayServer =
this.appRoot.serverList.find((displayServer: DisplayServer) => displayServer.isManaged);
this.showServerFromRepository(displayServer);
for (const server of managedServers) {
this.addServer(server);
}
this.showServer(managedServers[0]);
} else {
this.showCreateServer();
}
@ -692,18 +694,66 @@ export class App {
}
}
}
private async startDigitalOceanCreateServer() {
const accessToken = this.digitalOceanTokenManager.getStoredToken();
if (!accessToken) {
console.error('DigitalOcean accessToken not found');
return;
}
this.showCreateServer();
}
// private async connectToDigitalOcean() {
// let accessToken = this.digitalOceanTokenManager.getStoredToken();
// if (accessToken) {
// const managedServers = await this.enterDigitalOceanMode(accessToken);
// for (let server of managedServers) {
// // TODO: This is duplicating the servers
// this.addServer(server);
// }
// this.showCreateServer();
// return;
// }
// const session = runDigitalOceanOauth();
// const handleOauthFlowCancelled = () => {
// session.cancel();
// this.clearCredentialsAndShowIntro();
// };
// this.appRoot.getAndShowDigitalOceanOauthFlow(handleOauthFlowCancelled);
// try {
// accessToken = await session.result;
// // Save accessToken to storage. DigitalOcean tokens
// // expire after 30 days, unless they are manually revoked by the user.
// // After 30 days the user will have to sign into DigitalOcean again.
// // Note we cannot yet use DigitalOcean refresh tokens, as they require
// // a client_secret to be stored on a server and not visible to end users
// // in client-side JS. More details at:
// // https://developers.digitalocean.com/documentation/oauth/#refresh-token-flow
// this.digitalOceanTokenManager.writeTokenToStorage(accessToken);
// const managedServers = await this.enterDigitalOceanMode(accessToken);
// if (managedServers.length > 0) {
// for (let server of managedServers) {
// this.addServer(server);
// }
// this.showServer(managedServers[0]);
// } else {
// this.showCreateServer();
// }
// } catch (error) {
// if (!session.isCancelled()) {
// this.clearCredentialsAndShowIntro();
// bringToFront();
// console.error(`DigitalOcean authentication failed: ${error}`);
// this.appRoot.showError(this.appRoot.localize('error-do-auth'));
// }
// }
// }
// Clears the credentials and returns to the intro screen.
private clearCredentialsAndShowIntro() {
this.digitalOceanTokenManager.removeTokenFromStorage();
// Remove display servers from storage.
this.displayServerRepository.listServers().then((displayServers: DisplayServer[]) => {
for (const displayServer of displayServers) {
if (displayServer.isManaged) {
this.removeServerFromDisplay(displayServer);
}
}
});
// Clear list
this.appRoot.serverList = this.appRoot.serverList.filter((ds) => !ds.isManaged);
// Reset UI
this.appRoot.adminEmail = '';
if (!!this.appRoot.selectedServer && this.appRoot.selectedServer.isManaged) {
@ -716,6 +766,7 @@ export class App {
// Opens the screen to create a server.
private showCreateServer() {
// TODO: Should validate account here.
const regionPicker = this.appRoot.getAndShowRegionPicker();
// The region picker initially shows all options as disabled. Options are enabled by this code,
// after checking which regions are available.
@ -735,55 +786,63 @@ export class App {
});
}
private showServerCreationProgress() {
// Set selected server, needed for cancel button.
this.selectedServer = this.serverBeingCreated;
this.appRoot.selectedServer = this.getDisplayServerBeingCreated();
// Update UI. Only show cancel button if the server has not yet finished
// installation, to prevent accidental deletion when restarting.
const showCancelButton = !this.serverBeingCreated.isInstallCompleted();
this.appRoot.showProgress(this.appRoot.selectedServer.name, showCancelButton);
}
private waitForManagedServerCreation(tryAgain = false): void {
this.serverBeingCreated.waitOnInstall(tryAgain)
.then(() => {
// Unset the instance variable before syncing the server so the UI does not display it.
const server = this.serverBeingCreated;
this.serverBeingCreated = null;
return this.syncAndShowServer(server);
// Returns a promise which fulfills once the DigitalOcean droplet is created.
// Shadowbox may not be fully installed once this promise is fulfilled.
public async createDigitalOceanServer(regionId: server.RegionId) {
const serverName = this.makeLocalizedServerName(regionId);
return this
.digitalOceanRetry(() => {
return this.digitalOceanRepository.createServer(regionId, serverName);
})
.then(async (server) => {
this.addServer(server);
this.showServer(server);
// TODO: Needs to poll as in waitForManagedServerCreation.
await server.waitOnInstall(false);
this.updateServer(server);
})
.catch((e) => {
console.log(e);
if (e instanceof errors.DeletedServerError) {
// The user deleted this server, no need to show an error or delete it again.
this.serverBeingCreated = null;
return;
}
let errorMessage = this.serverBeingCreated.isInstallCompleted() ?
this.appRoot.localize('error-server-unreachable-title') :
this.appRoot.localize('error-server-creation');
errorMessage += ` ${this.appRoot.localize('digitalocean-unreachable')}`;
this.appRoot
.showModalDialog(
null, // Don't display any title.
errorMessage,
[this.appRoot.localize('server-destroy'), this.appRoot.localize('retry')])
.then((clickedButtonIndex: number) => {
if (clickedButtonIndex === 0) { // user clicked 'Delete this server'
console.info('Deleting unreachable server');
this.serverBeingCreated.getHost().delete().then(() => {
this.serverBeingCreated = null;
this.showCreateServer();
});
} else if (clickedButtonIndex === 1) { // user clicked 'Try again'.
console.info('Retrying unreachable server');
this.waitForManagedServerCreation(true);
}
});
// Sanity check - this error is not expected to occur, as waitForManagedServerCreation
// has it's own error handling.
console.error('error from waitForManagedServerCreation');
return Promise.reject(e);
});
}
// private waitForManagedServerCreation(server: server.ManagedServer, tryAgain = false): void {
// server.waitOnInstall(tryAgain)
// .then(() => {
// // Update name on the server list.
// this.updateServer(server);
// })
// .catch((e) => {
// console.log(e);
// if (e instanceof errors.DeletedServerError) {
// return;
// }
// let errorMessage = server.isInstallCompleted() ?
// this.appRoot.localize('error-server-unreachable-title') :
// this.appRoot.localize('error-server-creation');
// errorMessage += ` ${this.appRoot.localize('digitalocean-unreachable')}`;
// this.appRoot
// .showModalDialog(
// null, // Don't display any title.
// errorMessage,
// [this.appRoot.localize('server-destroy'), this.appRoot.localize('retry')])
// .then((clickedButtonIndex: number) => {
// if (clickedButtonIndex === 0) { // user clicked 'Delete this server'
// console.info('Deleting unreachable server');
// server.getHost().delete().then(() => {
// this.showCreateServer();
// });
// } else if (clickedButtonIndex === 1) { // user clicked 'Try again'.
// console.info('Retrying unreachable server');
// this.waitForManagedServerCreation(server, true);
// }
// });
// });
// }
private getLocalizedCityName(regionId: server.RegionId) {
const cityId = digitalocean_server.GetCityId(regionId);
return this.appRoot.localize(`city-${cityId}`);
@ -794,53 +853,43 @@ export class App {
return this.appRoot.localize('server-name', 'serverLocation', serverLocation);
}
// Returns a promise which fulfills once the DigitalOcean droplet is created.
// Shadowbox may not be fully installed once this promise is fulfilled.
public createDigitalOceanServer(regionId: server.RegionId) {
const serverName = this.makeLocalizedServerName(regionId);
return this
.digitalOceanRetry(() => {
return this.digitalOceanRepository.createServer(regionId, serverName);
})
.then((server) => {
this.syncServerCreationToUi(server);
})
.catch((e) => {
// Sanity check - this error is not expected to occur, as waitForManagedServerCreation
// has it's own error handling.
console.error('error from waitForManagedServerCreation');
return Promise.reject(e);
});
}
private async showServer(server: server.Server): Promise<void> {
this.selectedServer = server;
const displayServer = this.makeDisplayServer(server);
this.appRoot.selectedServer = displayServer;
localStorage.setItem(LAST_DISPLAYED_SERVER_STORAGE_KEY, localId(server));
// Syncs a healthy `server` to the display and shows it.
private async syncAndShowServer(server: server.Server, timeoutMs = 250) {
const displayServer = await this.syncServerToDisplay(server);
await this.syncDisplayServersToUi();
this.showServerManagement(server, displayServer);
if (await server.isHealthy()) {
// Sync the server display in case it was previously unreachable.
await this.showServerManagement(server);
return;
}
if (isManagedServer(server)) {
const managedServer = server as server.ManagedServer;
if (!managedServer.isInstallCompleted()) {
this.appRoot.showProgress(displayServer.name, true);
return;
}
}
this.showServerUnreachable(server, displayServer);
}
// Show the server management screen. Assumes the server is healthy.
private async showServerManagement(
selectedServer: server.Server, selectedDisplayServer: DisplayServer) {
this.selectedServer = selectedServer;
this.appRoot.selectedServer = selectedDisplayServer;
this.displayServerRepository.storeLastDisplayedServerId(selectedDisplayServer.id);
private async showServerManagement(server: server.Server) {
// Show view and initialize fields from selectedServer.
const view = this.appRoot.getServerView(selectedDisplayServer.id);
const view = this.appRoot.getServerView(localId(server));
view.isServerReachable = true;
view.serverId = selectedServer.getServerId();
view.serverName = selectedServer.getName();
view.serverHostname = selectedServer.getHostnameForAccessKeys();
view.serverManagementApiUrl = selectedServer.getManagementApiUrl();
view.serverPortForNewAccessKeys = selectedServer.getPortForNewAccessKeys();
view.serverCreationDate = localizeDate(selectedServer.getCreatedDate(), this.appRoot.language);
view.serverVersion = selectedServer.getVersion();
view.accessKeyDataLimit = dataLimitToDisplayDataAmount(selectedServer.getAccessKeyDataLimit());
view.serverId = server.getServerId();
view.serverName = server.getName();
view.serverHostname = server.getHostnameForAccessKeys();
view.serverManagementApiUrl = server.getManagementApiUrl();
view.serverPortForNewAccessKeys = server.getPortForNewAccessKeys();
view.serverCreationDate = localizeDate(server.getCreatedDate(), this.appRoot.language);
view.serverVersion = server.getVersion();
view.accessKeyDataLimit = dataLimitToDisplayDataAmount(server.getAccessKeyDataLimit());
view.isAccessKeyDataLimitEnabled = !!view.accessKeyDataLimit;
view.showFeatureMetricsDisclaimer = selectedServer.getMetricsEnabled() &&
!selectedServer.getAccessKeyDataLimit() && !hasSeenFeatureMetricsNotification();
view.showFeatureMetricsDisclaimer = server.getMetricsEnabled() &&
!server.getAccessKeyDataLimit() && !hasSeenFeatureMetricsNotification();
const version = this.selectedServer.getVersion();
if (version) {
@ -849,9 +898,9 @@ export class App {
view.isHostnameEditable = semver.gte(version, CHANGE_HOSTNAME_VERSION);
}
if (isManagedServer(selectedServer)) {
if (isManagedServer(server)) {
view.isServerManaged = true;
const host = selectedServer.getHost();
const host = server.getHost();
view.monthlyCost = host.getMonthlyCost().usd;
view.monthlyOutboundTransferBytes =
host.getMonthlyOutboundTransferLimit().terabytes * (10 ** 12);
@ -860,17 +909,17 @@ export class App {
view.isServerManaged = false;
}
view.metricsEnabled = selectedServer.getMetricsEnabled();
view.metricsEnabled = server.getMetricsEnabled();
this.appRoot.showServerView();
this.showMetricsOptInWhenNeeded(selectedServer, view);
this.showMetricsOptInWhenNeeded(server, view);
// Load "My Connection" and other access keys.
try {
const serverAccessKeys = await selectedServer.listAccessKeys();
const serverAccessKeys = await server.listAccessKeys();
view.accessKeyRows = serverAccessKeys.map(this.convertToUiAccessKey.bind(this));
if (!view.accessKeyDataLimit) {
view.accessKeyDataLimit = dataLimitToDisplayDataAmount(
await computeDefaultAccessKeyDataLimit(selectedServer, serverAccessKeys));
await computeDefaultAccessKeyDataLimit(server, serverAccessKeys));
}
// Show help bubbles once the page has rendered.
setTimeout(() => {
@ -881,7 +930,45 @@ export class App {
this.appRoot.showError(this.appRoot.localize('error-keys-get'));
}
this.showTransferStats(selectedServer, view);
this.showTransferStats(server, view);
}
// private showServerCreationProgress(server: server.ManagedServer, displayServer:
// DisplayServer) {
// this.appRoot.showProgress(server.getName(), true);
// this.waitForManagedServerCreation();
// }
private showServerUnreachable(server: server.Server, displayServer: DisplayServer) {
// Display the unreachable server state within the server view.
const serverView = this.appRoot.getServerView(displayServer.id) as ServerView;
serverView.isServerReachable = false;
serverView.isServerManaged = isManagedServer(server);
serverView.serverName = displayServer.name; // Don't get the name from the remote server.
serverView.retryDisplayingServer = async () => {
this.showServer(server);
// // Refresh the server list if the server is managed, it may have been deleted outside the
// // app.
// let serverExists = true;
// if (serverView.isServerManaged && !!this.digitalOceanRepository) {
// await this.digitalOceanRepository.listServers();
// serverExists = !!(await this.getServerFromRepository(displayServer));
// }
// if (serverExists) {
// await this.showServer(server);
// } else {
// // Server has been deleted outside the app.
// this.appRoot.showError(
// this.appRoot.localize('error-server-removed', 'serverName', displayServer.name));
// this.removeServerFromDisplayList(server);
// this.selectedServer = null;
// this.appRoot.selectedServer = null;
// this.showIntro();
// }
};
this.selectedServer = server;
this.appRoot.selectedServer = displayServer;
this.appRoot.showServerView();
}
private showMetricsOptInWhenNeeded(selectedServer: server.Server, serverView: ServerView) {
@ -1108,15 +1195,13 @@ export class App {
// are adding an existing server. Query the repository instead to treat the UI accordingly.
const storedServer = this.manualServerRepository.findServer(serverConfig);
if (!!storedServer) {
return this.syncServerToDisplay(storedServer).then((displayServer) => {
this.appRoot.showNotification(this.appRoot.localize('notification-server-exists'), 5000);
this.showServer(storedServer, displayServer);
});
this.appRoot.showNotification(this.appRoot.localize('notification-server-exists'), 5000);
return this.showServer(storedServer);
}
return this.manualServerRepository.addServer(serverConfig).then((manualServer) => {
return manualServer.isHealthy().then((isHealthy) => {
if (isHealthy) {
return this.syncAndShowServer(manualServer);
return this.showServer(manualServer);
} else {
// Remove inaccessible manual server from local storage if it was just created.
manualServer.forget();
@ -1141,6 +1226,7 @@ export class App {
private deleteSelectedServer() {
const serverToDelete = this.selectedServer;
const serverId = localId(serverToDelete);
if (!isManagedServer(serverToDelete)) {
const msg = 'cannot delete non-ManagedServer';
console.error(msg);
@ -1156,7 +1242,7 @@ export class App {
})
.then(
() => {
this.removeServerFromDisplay(this.appRoot.selectedServer);
this.removeServer(serverId);
this.appRoot.selectedServer = null;
this.selectedServer = null;
this.showIntro();
@ -1175,6 +1261,7 @@ export class App {
private forgetSelectedServer() {
const serverToForget = this.selectedServer;
const serverId = localId(serverToForget);
if (!isManualServer(serverToForget)) {
const msg = 'cannot forget non-ManualServer';
console.error(msg);
@ -1186,7 +1273,7 @@ export class App {
const confirmationButton = this.appRoot.localize('remove');
this.appRoot.getConfirmation(confirmationTitle, confirmationText, confirmationButton, () => {
serverToForget.forget();
this.removeServerFromDisplay(this.appRoot.selectedServer);
this.removeServer(serverId);
this.appRoot.selectedServer = null;
this.selectedServer = null;
this.showIntro();
@ -1209,11 +1296,13 @@ export class App {
}
private async renameServer(newName: string) {
const view = this.appRoot.getServerView(this.appRoot.selectedServer.id);
const serverToRename = this.selectedServer;
const serverId = this.appRoot.selectedServer.id;
const view = this.appRoot.getServerView(serverId);
try {
await this.selectedServer.setName(newName);
await serverToRename.setName(newName);
view.serverName = newName;
this.syncAndShowServer(this.selectedServer);
this.updateServer(serverToRename);
} catch (error) {
console.error(`Failed to rename server: ${error}`);
this.appRoot.showError(this.appRoot.localize('error-server-rename'));
@ -1231,8 +1320,7 @@ export class App {
throw new Error(msg);
}
serverToCancel.getHost().delete().then(() => {
this.serverBeingCreated = null;
this.removeServerFromDisplay(this.appRoot.selectedServer);
this.removeServer(localId(serverToCancel));
this.appRoot.selectedServer = null;
this.showCreateServer();
});

View file

@ -1,138 +0,0 @@
// Copyright 2018 The Outline 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 {InMemoryStorage} from '../infrastructure/memory_storage';
import {DisplayServerRepository} from './display_server';
// Use this helper to compare `DisplayServer`s when we don't care about the
// `isSynced` property.
const objectContaining = jasmine.objectContaining;
describe('DisplayServerRepository', () => {
it('adds and finds servers', () => {
const displayServer = {id: 'id', name: 'name', isManaged: false};
const repository = new DisplayServerRepository(new InMemoryStorage());
repository.addServer(displayServer);
expect(displayServer).toEqual(repository.findServer(displayServer.id));
});
it('lists servers', () => {
const displayServer1 = {id: 'id1', name: 'name1', isManaged: false};
const displayServer2 = {id: 'id2', name: 'name2', isManaged: true};
const store = new Map([[
DisplayServerRepository.SERVERS_STORAGE_KEY,
JSON.stringify([displayServer1, displayServer2])
]]);
const repository = new DisplayServerRepository(new InMemoryStorage(store));
repository.listServers().then((servers) => {
expect(servers).toContain(objectContaining(displayServer1));
expect(servers).toContain(objectContaining(displayServer2));
});
});
it('throws when adding duplicate servers', () => {
const displayServer1 = {id: 'id', name: 'name', isManaged: true};
const displayServer2 = {id: 'id', name: 'name', isManaged: true};
const repository = new DisplayServerRepository(new InMemoryStorage());
repository.addServer(displayServer1);
const addDuplicateServer = () => {
repository.addServer(displayServer2);
};
expect(addDuplicateServer).toThrow();
});
it('throws when adding servers missing properties', () => {
const displayServer = {id: 'id', name: 'name', isManaged: true};
delete displayServer['name']; // Appease compiler.
const repository = new DisplayServerRepository(new InMemoryStorage());
const addMalformedServer = () => {
repository.addServer(displayServer);
};
expect(addMalformedServer).toThrow();
});
it('loads existing servers', () => {
const displayServer1 = {id: 'id1', name: 'name1', isManaged: false};
const displayServer2 = {id: 'id2', name: 'name2', isManaged: true};
const store = new Map([[
DisplayServerRepository.SERVERS_STORAGE_KEY,
JSON.stringify([displayServer1, displayServer2])
]]);
const repository = new DisplayServerRepository(new InMemoryStorage(store));
expect(repository.findServer(displayServer1.id)).toEqual(objectContaining(displayServer1));
expect(repository.findServer(displayServer2.id)).toEqual(objectContaining(displayServer2));
});
it('loads existing servers unsynced', () => {
// Initialize isSynced to true to simulate a persisted synced server.
const displayServer = {id: 'id', name: 'name', isManaged: false, isSynced: false};
const store =
new Map([[DisplayServerRepository.SERVERS_STORAGE_KEY, JSON.stringify([displayServer])]]);
const repository = new DisplayServerRepository(new InMemoryStorage(store));
const foundServer = repository.findServer(displayServer.id);
expect(foundServer.isSynced).toBeFalsy();
});
it('removes servers', () => {
const displayServerToKeep = {id: 'id0', name: 'name0', isManaged: false};
const displayServerToRemove = {id: 'id', name: 'name', isManaged: false};
const repository = new DisplayServerRepository(new InMemoryStorage());
repository.addServer(displayServerToKeep);
repository.addServer(displayServerToRemove);
expect(repository.findServer(displayServerToRemove.id)).toEqual(displayServerToRemove);
repository.removeServer(displayServerToRemove);
expect(repository.findServer(displayServerToRemove.id)).toBeUndefined();
expect(repository.findServer(displayServerToKeep.id))
.toEqual(objectContaining(displayServerToKeep));
});
it('persists servers', () => {
const displayServer = {id: 'id', name: 'name', isManaged: false};
const storage = new InMemoryStorage();
let repository = new DisplayServerRepository(storage);
repository.addServer(displayServer);
// Instantiate a new repository to validate that servers have been persisted to storage.
repository = new DisplayServerRepository(storage);
expect(repository.findServer(displayServer.id)).toEqual(objectContaining(displayServer));
});
it('persists the last displayed server ID', () => {
const serverId = 'serverId';
const storage = new InMemoryStorage();
let repository = new DisplayServerRepository(storage);
repository.storeLastDisplayedServerId(serverId);
expect(repository.getLastDisplayedServerId()).toEqual(serverId);
repository = new DisplayServerRepository(storage);
expect(repository.getLastDisplayedServerId()).toEqual(serverId);
});
it('removes the last displayed server ID', () => {
const serverId = 'serverId';
const storage = new InMemoryStorage();
const repository = new DisplayServerRepository(storage);
repository.storeLastDisplayedServerId(serverId);
expect(repository.getLastDisplayedServerId()).toEqual(serverId);
repository.removeLastDisplayedServerId();
expect(repository.getLastDisplayedServerId()).toBeNull();
});
});

View file

@ -12,96 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import * as server from '../model/server';
export interface DisplayServer {
id: string;
name: string;
isManaged: boolean;
isSynced?: boolean;
}
// Returns a `DisplayServer` corresponding to `server`.
export async function makeDisplayServer(server: server.Server) {
return {
id: server.getManagementApiUrl(),
name: await server.isHealthy().catch((e) => false) ? server.getName() :
server.getHostnameForAccessKeys(),
isManaged: !!(server as server.ManagedServer).getHost,
isSynced: true
};
}
// Persistence layer for `DisplayServer`. Caches the list of servers shown in the UI in case they
// cannot be accessed due to connectivity issues.
export class DisplayServerRepository {
static readonly SERVERS_STORAGE_KEY = 'displayServers';
static readonly LAST_DISPLAYED_SERVER_STORAGE_KEY = 'lastDisplayedServer';
private servers: DisplayServer[] = [];
constructor(private storage: Storage = localStorage) {
this.loadServers();
}
listServers(): Promise<DisplayServer[]> {
// Copy the server array; resolving with the instance variable may lead to races in `findServer`
return Promise.resolve(JSON.parse(JSON.stringify(this.servers)));
}
addServer(server: DisplayServer) {
if (!server || !server.id || !server.name) {
throw new Error('Failed to add display server, one or more properties missing');
}
if (this.findServer(server.id)) {
throw new Error(`Display server already stored`);
}
this.servers.push(server);
this.storeServers();
}
removeServer(serverToRemove: DisplayServer) {
this.servers = this.servers.filter((server: DisplayServer) => {
return server.id !== serverToRemove.id;
});
this.storeServers();
}
findServer(serverId: string): DisplayServer {
return this.servers.find(server => server.id === serverId);
}
storeLastDisplayedServerId(serverId: string) {
this.storage.setItem(DisplayServerRepository.LAST_DISPLAYED_SERVER_STORAGE_KEY, serverId);
}
getLastDisplayedServerId(): string {
return this.storage.getItem(DisplayServerRepository.LAST_DISPLAYED_SERVER_STORAGE_KEY);
}
removeLastDisplayedServerId() {
this.storage.removeItem(DisplayServerRepository.LAST_DISPLAYED_SERVER_STORAGE_KEY);
}
// Loads the servers from storage. Does *not* throw because it is only called on the constructor,
// and the repository is created before the app starts.
private loadServers() {
const serversJson = this.storage.getItem(DisplayServerRepository.SERVERS_STORAGE_KEY);
if (!serversJson) {
return;
}
try {
// We need the servers' `isSynced` property be false on load.
this.servers = JSON.parse(serversJson).map((server: DisplayServer) => {
server.isSynced = false;
return server;
});
} catch (e) {
console.error('Error loading local servers from storage');
}
}
private storeServers() {
this.storage.setItem(DisplayServerRepository.SERVERS_STORAGE_KEY, JSON.stringify(this.servers));
}
}

View file

@ -21,7 +21,6 @@ import {getSentryApiUrl} from '../infrastructure/sentry';
import {App} from './app';
import {DigitalOceanTokenManager} from './digitalocean_oauth';
import * as digitalocean_server from './digitalocean_server';
import {DisplayServerRepository} from './display_server';
import {ManualServerRepository} from './manual_server';
import {AppRoot} from './ui_components/app-root.js';
@ -123,7 +122,7 @@ document.addEventListener('WebComponentsReady', () => {
new App(
appRoot, version, digitalocean_api.createDigitalOceanSession,
digitalOceanServerRepositoryFactory, new ManualServerRepository('manualServers'),
new DisplayServerRepository(), new DigitalOceanTokenManager())
new DigitalOceanTokenManager())
.start();
});

View file

@ -100,7 +100,7 @@ export class ShadowboxServer implements server.Server {
}
getName(): string {
return this.serverConfig.name;
return this.serverConfig?.name;
}
setName(name: string): Promise<void> {
@ -178,7 +178,8 @@ export class ShadowboxServer implements server.Server {
getHostnameForAccessKeys(): string {
try {
return this.serverConfig.hostnameForAccessKeys || new URL(this.managementApiAddress).hostname;
return this.serverConfig?.hostnameForAccessKeys ??
new URL(this.managementApiAddress).hostname;
} catch (e) {
return '';
}
@ -207,9 +208,9 @@ export class ShadowboxServer implements server.Server {
});
}
private getServerConfig(): Promise<ServerConfig> {
private async getServerConfig(): Promise<ServerConfig> {
console.info('Retrieving server configuration');
return this.apiRequest<ServerConfig>('server');
return await this.apiRequest<ServerConfig>('server');
}
protected setManagementApiUrl(apiAddress: string): void {

View file

@ -279,7 +279,11 @@ Polymer({
},
connectToDigitalOceanTapped: function() {
this.fire('ConnectToDigitalOcean');
if (this.isSignedInToDigitalOcean) {
this.fire('StartDigitalOceanCreateServerRequested');
} else {
this.fire('StartDigitalOceanConnectAccountRequested');
}
},
setUpGenericCloudProviderTapped: function() {

523
yarn.lock
View file

@ -1259,6 +1259,11 @@ acorn@^6.4.1:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474"
integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==
after@0.8.2:
version "0.8.2"
resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f"
integrity sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=
agent-base@5:
version "5.1.1"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-5.1.1.tgz#e8fb3f242959db44d63be665db7a8e739537a32c"
@ -1563,6 +1568,11 @@ array-unique@^0.3.2:
resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
arraybuffer.slice@~0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz#3bbc4275dd584cc1b10809b89d4e8b63a69e7675"
integrity sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==
arrify@^1.0.0, arrify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
@ -1696,6 +1706,11 @@ bach@^1.0.0:
async-settle "^1.0.0"
now-and-later "^2.0.0"
backo2@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"
integrity sha1-MasayLEpNjRj41s+u2n038+6eUc=
balanced-match@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
@ -1706,11 +1721,26 @@ base-64@^0.1.0:
resolved "https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb"
integrity sha1-eAqZyE59YAJgNhURxId2E78k9rs=
base64-arraybuffer@0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz#9818c79e059b1355f97e0428a017c838e90ba812"
integrity sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=
base64-arraybuffer@0.1.5:
version "0.1.5"
resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8"
integrity sha1-c5JncZI7Whl0etZmqlzUv5xunOg=
base64-js@^1.0.2, base64-js@^1.3.0:
version "1.3.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==
base64id@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6"
integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==
base@^0.11.1:
version "0.11.2"
resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f"
@ -1736,6 +1766,13 @@ bcrypt-pbkdf@^1.0.0:
dependencies:
tweetnacl "^0.14.3"
better-assert@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/better-assert/-/better-assert-1.0.2.tgz#40866b9e1b9e0b55b481894311e68faffaebc522"
integrity sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=
dependencies:
callsite "1.0.0"
big.js@^5.1.2, big.js@^5.2.2:
version "5.2.2"
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
@ -1797,6 +1834,11 @@ bl@^1.0.0:
readable-stream "^2.3.5"
safe-buffer "^5.1.1"
blob@0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683"
integrity sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==
bluebird-lst@^1.0.9:
version "1.0.9"
resolved "https://registry.yarnpkg.com/bluebird-lst/-/bluebird-lst-1.0.9.tgz#a64a0e4365658b9ab5fe875eb9dfb694189bb41c"
@ -1824,7 +1866,7 @@ bn.js@^5.1.1:
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.2.tgz#c9686902d3c9a27729f43ab10f9d79c2004da7b0"
integrity sha512-40rZaf3bUNKTVYu9sIeeEGOg7g14Yvnj9kH7b50EiwX0Q7A6umbvfI5tvHaOERH0XigqKkfLkFQxzb4e6CIXnA==
body-parser@1.19.0, body-parser@^1.18.3:
body-parser@1.19.0, body-parser@^1.18.3, body-parser@^1.19.0:
version "1.19.0"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==
@ -1900,7 +1942,7 @@ braces@^2.3.1, braces@^2.3.2:
split-string "^3.0.2"
to-regex "^3.0.1"
braces@^3.0.1, braces@~3.0.2:
braces@^3.0.1, braces@^3.0.2, braces@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
@ -2190,6 +2232,11 @@ caller-path@^2.0.0:
dependencies:
caller-callsite "^2.0.0"
callsite@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20"
integrity sha1-KAOY5dZkvXQDi28JBRU+borxvCA=
callsites@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50"
@ -2308,6 +2355,21 @@ chokidar@^3.4.0:
optionalDependencies:
fsevents "~2.1.2"
chokidar@^3.4.2:
version "3.4.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.3.tgz#c1df38231448e45ca4ac588e6c79573ba6a57d5b"
integrity sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ==
dependencies:
anymatch "~3.1.1"
braces "~3.0.2"
glob-parent "~5.1.0"
is-binary-path "~2.1.0"
is-glob "~4.0.1"
normalize-path "~3.0.0"
readdirp "~3.5.0"
optionalDependencies:
fsevents "~2.1.2"
chownr@^1.1.1:
version "1.1.4"
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
@ -2406,6 +2468,15 @@ clone-buffer@^1.0.0:
resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58"
integrity sha1-4+JbIHrE5wGvch4staFnksrD3Fg=
clone-deep@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387"
integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==
dependencies:
is-plain-object "^2.0.4"
kind-of "^6.0.2"
shallow-clone "^3.0.0"
clone-response@1.0.2, clone-response@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b"
@ -2483,6 +2554,11 @@ color-support@^1.1.3:
resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2"
integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==
colors@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
colors@~0.6.0-1:
version "0.6.2"
resolved "https://registry.yarnpkg.com/colors/-/colors-0.6.2.tgz#2423fe6678ac0c5dae8852e5d0e5be08c997abcc"
@ -2515,11 +2591,26 @@ commondir@^1.0.1:
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=
component-emitter@^1.2.1:
component-bind@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1"
integrity sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=
component-emitter@1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=
component-emitter@^1.2.1, component-emitter@~1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
component-inherit@0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143"
integrity sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=
compressible@^2.0.12, compressible@~2.0.16:
version "2.0.18"
resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba"
@ -2602,6 +2693,16 @@ connect-history-api-fallback@^1.6.0:
resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc"
integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==
connect@^3.7.0:
version "3.7.0"
resolved "https://registry.yarnpkg.com/connect/-/connect-3.7.0.tgz#5d49348910caa5e07a01800b030d0c35f20484f8"
integrity sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==
dependencies:
debug "2.6.9"
finalhandler "1.1.2"
parseurl "~1.3.3"
utils-merge "1.0.1"
console-browserify@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336"
@ -2636,16 +2737,16 @@ cookie-signature@1.0.6:
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw=
cookie@0.3.1, cookie@^0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb"
integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=
cookie@0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba"
integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==
cookie@^0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb"
integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=
copy-concurrently@^1.0.0:
version "1.0.5"
resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0"
@ -2880,6 +2981,11 @@ currently-unhandled@^0.4.1:
dependencies:
array-find-index "^1.0.1"
custom-event@~1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"
integrity sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=
cyclist@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
@ -2905,6 +3011,16 @@ date-and-time@^0.6.3:
resolved "https://registry.yarnpkg.com/date-and-time/-/date-and-time-0.6.3.tgz#2daee52df67c28bd93bce862756ac86b68cf4237"
integrity sha512-lcWy3AXDRJOD7MplwZMmNSRM//kZtJaLz4n6D1P5z9wEmZGBKhJRBIr1Xs9KNQJmdXPblvgffynYji4iylUTcA==
date-format@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/date-format/-/date-format-2.1.0.tgz#31d5b5ea211cf5fd764cd38baf9d033df7e125cf"
integrity sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA==
date-format@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/date-format/-/date-format-3.0.0.tgz#eb8780365c7d2b1511078fb491e6479780f3ad95"
integrity sha512-eyTcpKOcamdhWJXj56DpQMo1ylSQpcGtGKXcU0Tb97+K56/CF5amAqqqNj0+KvA0iw2ynxtHWFsPDSClCxe48w==
debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
@ -2912,7 +3028,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9:
dependencies:
ms "2.0.0"
debug@4, debug@^4.1.0, debug@^4.1.1:
debug@4, debug@^4.1.0, debug@^4.1.1, debug@~4.1.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
@ -2926,6 +3042,13 @@ debug@^3.0.0, debug@^3.1.0, debug@^3.1.1, debug@^3.2.5:
dependencies:
ms "^2.1.1"
debug@~3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
dependencies:
ms "2.0.0"
decamelize@^1.1.1, decamelize@^1.1.2, decamelize@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
@ -3126,6 +3249,11 @@ detect-node@^2.0.4:
resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c"
integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==
di@^0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c"
integrity sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=
diff@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
@ -3187,6 +3315,16 @@ dom-converter@^0.2:
dependencies:
utila "~0.4"
dom-serialize@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b"
integrity sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=
dependencies:
custom-event "~1.0.0"
ent "~2.2.0"
extend "^3.0.0"
void-elements "^2.0.0"
dom-serializer@0:
version "0.2.2"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51"
@ -3508,6 +3646,46 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1:
dependencies:
once "^1.4.0"
engine.io-client@~3.4.0:
version "3.4.4"
resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.4.4.tgz#77d8003f502b0782dd792b073a4d2cf7ca5ab967"
integrity sha512-iU4CRr38Fecj8HoZEnFtm2EiKGbYZcPn3cHxqNGl/tmdWRf60KhK+9vE0JeSjgnlS/0oynEfLgKbT9ALpim0sQ==
dependencies:
component-emitter "~1.3.0"
component-inherit "0.0.3"
debug "~3.1.0"
engine.io-parser "~2.2.0"
has-cors "1.1.0"
indexof "0.0.1"
parseqs "0.0.6"
parseuri "0.0.6"
ws "~6.1.0"
xmlhttprequest-ssl "~1.5.4"
yeast "0.1.2"
engine.io-parser@~2.2.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-2.2.1.tgz#57ce5611d9370ee94f99641b589f94c97e4f5da7"
integrity sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg==
dependencies:
after "0.8.2"
arraybuffer.slice "~0.0.7"
base64-arraybuffer "0.1.4"
blob "0.0.5"
has-binary2 "~1.0.2"
engine.io@~3.4.0:
version "3.4.2"
resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-3.4.2.tgz#8fc84ee00388e3e228645e0a7d3dfaeed5bd122c"
integrity sha512-b4Q85dFkGw+TqgytGPrGgACRUhsdKc9S9ErRAXpPGy/CXKs4tYoHDkvIRdsseAF7NjfVwjRFIn6KTnbw7LwJZg==
dependencies:
accepts "~1.3.4"
base64id "2.0.0"
cookie "0.3.1"
debug "~4.1.0"
engine.io-parser "~2.2.0"
ws "^7.1.2"
enhanced-resolve@4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz#41c7e0bfdfe74ac1ffe1e57ad6a5c6c9f3742a7f"
@ -3526,7 +3704,7 @@ enhanced-resolve@^4.0.0, enhanced-resolve@^4.1.0:
memory-fs "^0.5.0"
tapable "^1.0.0"
ent@^2.2.0:
ent@^2.2.0, ent@~2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d"
integrity sha1-6WQhkyWiHQX0RGai9obtbOX13R0=
@ -4059,7 +4237,7 @@ fill-range@^7.0.1:
dependencies:
to-regex-range "^5.0.1"
finalhandler@~1.1.2:
finalhandler@1.1.2, finalhandler@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==
@ -4157,6 +4335,11 @@ flagged-respawn@^1.0.0:
resolved "https://registry.yarnpkg.com/flagged-respawn/-/flagged-respawn-1.0.1.tgz#e7de6f1279ddd9ca9aac8a5971d618606b3aab41"
integrity sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==
flatted@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138"
integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==
flush-write-stream@^1.0.0, flush-write-stream@^1.0.2:
version "1.1.1"
resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8"
@ -4470,7 +4653,7 @@ glob@^6.0.1:
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4:
glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
version "7.1.6"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
@ -4688,7 +4871,7 @@ got@^9.6.0:
to-readable-stream "^1.0.0"
url-parse-lax "^3.0.0"
graceful-fs@^4.0.0, graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0:
graceful-fs@^4.0.0, graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.4:
version "4.2.4"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
@ -4789,6 +4972,18 @@ has-ansi@^2.0.0:
dependencies:
ansi-regex "^2.0.0"
has-binary2@~1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/has-binary2/-/has-binary2-1.0.3.tgz#7776ac627f3ea77250cfc332dab7ddf5e4f5d11d"
integrity sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==
dependencies:
isarray "2.0.1"
has-cors@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39"
integrity sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=
has-flag@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
@ -5041,7 +5236,7 @@ http-proxy-middleware@0.19.1:
lodash "^4.17.11"
micromatch "^3.1.10"
http-proxy@^1.17.0:
http-proxy@^1.17.0, http-proxy@^1.18.1:
version "1.18.1"
resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549"
integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==
@ -5195,6 +5390,11 @@ indexes-of@^1.0.1:
resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc=
indexof@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d"
integrity sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=
infer-owner@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467"
@ -5708,7 +5908,12 @@ isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
isbinaryfile@^4.0.4:
isarray@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.1.tgz#a37d94ed9cda2d59865c9f76fe596ee1f338741e"
integrity sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=
isbinaryfile@^4.0.4, isbinaryfile@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.6.tgz#edcb62b224e2b4710830b67498c8e4e5a4d2610b"
integrity sha512-ORrEy+SNVqUhrCaal4hA4fBzhggQQ+BaLntyPOdoEiwlKZW9BZiJXjg3RMiruE4tPEI3pyVPpySHQF/dKWperg==
@ -5762,6 +5967,11 @@ jake@^10.6.1:
filelist "^1.0.1"
minimatch "^3.0.4"
jasmine-core@^3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.6.0.tgz#491f3bb23941799c353ceb7a45b38a950ebc5a20"
integrity sha512-8uQYa7zJN8hq9z+g8z1bqCfdC8eoDAeVnM5sfqs7KHv9/ifoJ500m018fpFc7RDaO6SWCLCXwo/wPSNcdYTgcw==
jasmine-core@~3.5.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.5.0.tgz#132c23e645af96d85c8bca13c8758b18429fc1e4"
@ -5936,6 +6146,61 @@ jws@^3.1.5:
jwa "^1.4.1"
safe-buffer "^5.0.1"
karma-chrome-launcher@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-3.1.0.tgz#805a586799a4d05f4e54f72a204979f3f3066738"
integrity sha512-3dPs/n7vgz1rxxtynpzZTvb9y/GIaW8xjAwcIGttLbycqoFtI7yo1NGnQi6oFTherRE+GIhCAHZC4vEqWGhNvg==
dependencies:
which "^1.2.1"
karma-jasmine@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/karma-jasmine/-/karma-jasmine-4.0.1.tgz#b99e073b6d99a5196fc4bffc121b89313b0abd82"
integrity sha512-h8XDAhTiZjJKzfkoO1laMH+zfNlra+dEQHUAjpn5JV1zCPtOIVWGQjLBrqhnzQa/hrU2XrZwSyBa6XjEBzfXzw==
dependencies:
jasmine-core "^3.6.0"
karma-webpack@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/karma-webpack/-/karma-webpack-4.0.2.tgz#23219bd95bdda853e3073d3874d34447c77bced0"
integrity sha512-970/okAsdUOmiMOCY8sb17A2I8neS25Ad9uhyK3GHgmRSIFJbDcNEFE8dqqUhNe9OHiCC9k3DMrSmtd/0ymP1A==
dependencies:
clone-deep "^4.0.1"
loader-utils "^1.1.0"
neo-async "^2.6.1"
schema-utils "^1.0.0"
source-map "^0.7.3"
webpack-dev-middleware "^3.7.0"
karma@^5.2.3:
version "5.2.3"
resolved "https://registry.yarnpkg.com/karma/-/karma-5.2.3.tgz#3264024219bad2728e92542e0058a2492d7a46e4"
integrity sha512-tHdyFADhVVPBorIKCX8A37iLHxc6RBRphkSoQ+MLKdAtFn1k97tD8WUGi1KlEtDZKL3hui0qhsY9HXUfSNDYPQ==
dependencies:
body-parser "^1.19.0"
braces "^3.0.2"
chokidar "^3.4.2"
colors "^1.4.0"
connect "^3.7.0"
di "^0.0.1"
dom-serialize "^2.2.1"
glob "^7.1.6"
graceful-fs "^4.2.4"
http-proxy "^1.18.1"
isbinaryfile "^4.0.6"
lodash "^4.17.19"
log4js "^6.2.1"
mime "^2.4.5"
minimatch "^3.0.4"
qjobs "^1.2.0"
range-parser "^1.2.1"
rimraf "^3.0.2"
socket.io "^2.3.0"
source-map "^0.6.1"
tmp "0.2.1"
ua-parser-js "0.7.22"
yargs "^15.3.1"
kew@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/kew/-/kew-0.7.0.tgz#79d93d2d33363d6fdd2970b335d9141ad591d79b"
@ -6104,7 +6369,7 @@ loader-utils@1.2.3:
emojis-list "^2.0.0"
json5 "^1.0.1"
loader-utils@^1.0.2, loader-utils@^1.2.3:
loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.3:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613"
integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==
@ -6152,6 +6417,22 @@ lodash@^4.13.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"
integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==
lodash@^4.17.19:
version "4.17.20"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
log4js@^6.2.1:
version "6.3.0"
resolved "https://registry.yarnpkg.com/log4js/-/log4js-6.3.0.tgz#10dfafbb434351a3e30277a00b9879446f715bcb"
integrity sha512-Mc8jNuSFImQUIateBFwdOQcmC6Q5maU0VVvdC2R6XMb66/VnT+7WS4D/0EeNMZu1YODmJe5NIn2XftCzEocUgw==
dependencies:
date-format "^3.0.0"
debug "^4.1.1"
flatted "^2.0.1"
rfdc "^1.1.4"
streamroller "^2.2.4"
loglevel@^1.6.8:
version "1.6.8"
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.8.tgz#8a25fb75d092230ecd4457270d80b54e28011171"
@ -6414,6 +6695,11 @@ mime@^2.2.0, mime@^2.4.3, mime@^2.4.4:
resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1"
integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==
mime@^2.4.5:
version "2.4.7"
resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.7.tgz#962aed9be0ed19c91fd7dc2ece5d7f4e89a90d74"
integrity sha512-dhNd1uA2u397uQk3Nv5LM4lm93WYDUXFn3Fu291FJerns4jyTudqhIWe4W04YLy7Uk1tm1Ore04NpjRvQp/NPA==
mimic-fn@^2.0.0, mimic-fn@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
@ -6786,6 +7072,11 @@ object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
object-component@0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291"
integrity sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=
object-copy@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c"
@ -7168,6 +7459,30 @@ parse-passwd@^1.0.0:
resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=
parseqs@0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d"
integrity sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=
dependencies:
better-assert "~1.0.0"
parseqs@0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.6.tgz#8e4bb5a19d1cdc844a08ac974d34e273afa670d5"
integrity sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==
parseuri@0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.5.tgz#80204a50d4dbb779bfdc6ebe2778d90e4bce320a"
integrity sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=
dependencies:
better-assert "~1.0.0"
parseuri@0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.6.tgz#e1496e829e3ac2ff47f39a4dd044b32823c4a25a"
integrity sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==
parseurl@~1.3.2, parseurl@~1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
@ -7689,6 +8004,11 @@ pupa@^2.0.1:
dependencies:
escape-goat "^2.0.0"
qjobs@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071"
integrity sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==
qs@6.7.0:
version "6.7.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
@ -7856,6 +8176,13 @@ readdirp@~3.4.0:
dependencies:
picomatch "^2.2.1"
readdirp@~3.5.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e"
integrity sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==
dependencies:
picomatch "^2.2.1"
rechoir@^0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384"
@ -8153,6 +8480,11 @@ retry@^0.12.0:
resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b"
integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=
rfdc@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.1.4.tgz#ba72cc1367a0ccd9cf81a870b3b58bd3ad07f8c2"
integrity sha512-5C9HXdzK8EAqN7JDif30jqsBzavB7wLpaubisuQIGHWf2gUXSpzy6ArX/+Da8RjFpagWsCn+pIgxTMAmKw9Zug==
rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.3:
version "2.7.1"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
@ -8160,6 +8492,13 @@ rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.3:
dependencies:
glob "^7.1.3"
rimraf@^3.0.0, rimraf@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
dependencies:
glob "^7.1.3"
rimraf@~2.4.0:
version "2.4.5"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.4.5.tgz#ee710ce5d93a8fdb856fb5ea8ff0e2d75934b2da"
@ -8442,6 +8781,13 @@ sha.js@^2.4.0, sha.js@^2.4.8:
inherits "^2.0.1"
safe-buffer "^5.0.1"
shallow-clone@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3"
integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==
dependencies:
kind-of "^6.0.2"
shebang-command@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
@ -8516,6 +8862,61 @@ snapdragon@^0.8.1:
source-map-resolve "^0.5.0"
use "^3.1.0"
socket.io-adapter@~1.1.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-1.1.2.tgz#ab3f0d6f66b8fc7fca3959ab5991f82221789be9"
integrity sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g==
socket.io-client@2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.3.0.tgz#14d5ba2e00b9bcd145ae443ab96b3f86cbcc1bb4"
integrity sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA==
dependencies:
backo2 "1.0.2"
base64-arraybuffer "0.1.5"
component-bind "1.0.0"
component-emitter "1.2.1"
debug "~4.1.0"
engine.io-client "~3.4.0"
has-binary2 "~1.0.2"
has-cors "1.1.0"
indexof "0.0.1"
object-component "0.0.3"
parseqs "0.0.5"
parseuri "0.0.5"
socket.io-parser "~3.3.0"
to-array "0.1.4"
socket.io-parser@~3.3.0:
version "3.3.1"
resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.3.1.tgz#f07d9c8cb3fb92633aa93e76d98fd3a334623199"
integrity sha512-1QLvVAe8dTz+mKmZ07Swxt+LAo4Y1ff50rlyoEx00TQmDFVQYPfcqGvIDJLGaBdhdNCecXtyKpD+EgKGcmmbuQ==
dependencies:
component-emitter "~1.3.0"
debug "~3.1.0"
isarray "2.0.1"
socket.io-parser@~3.4.0:
version "3.4.1"
resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.4.1.tgz#b06af838302975837eab2dc980037da24054d64a"
integrity sha512-11hMgzL+WCLWf1uFtHSNvliI++tcRUWdoeYuwIl+Axvwy9z2gQM+7nJyN3STj1tLj5JyIUH8/gpDGxzAlDdi0A==
dependencies:
component-emitter "1.2.1"
debug "~4.1.0"
isarray "2.0.1"
socket.io@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-2.3.0.tgz#cd762ed6a4faeca59bc1f3e243c0969311eb73fb"
integrity sha512-2A892lrj0GcgR/9Qk81EaY2gYhCBxurV0PfmmESO6p27QPrUK1J3zdns+5QPqvUYK2q657nSj0guoIil9+7eFg==
dependencies:
debug "~4.1.0"
engine.io "~3.4.0"
has-binary2 "~1.0.2"
socket.io-adapter "~1.1.0"
socket.io-client "2.3.0"
socket.io-parser "~3.4.0"
sockjs-client@1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.4.0.tgz#c9f2568e19c8fd8173b4997ea3420e0bb306c7d5"
@ -8597,6 +8998,11 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
source-map@^0.7.3:
version "0.7.3"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
sparkles@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/sparkles/-/sparkles-1.0.1.tgz#008db65edce6c50eec0c5e228e1945061dd0437c"
@ -8788,6 +9194,15 @@ stream-transform@^2.0.1:
dependencies:
mixme "^0.3.1"
streamroller@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-2.2.4.tgz#c198ced42db94086a6193608187ce80a5f2b0e53"
integrity sha512-OG79qm3AujAM9ImoqgWEY1xG4HX+Lw+yY6qZj9R1K2mhF5bEmQ849wvrb+4vt4jLMLzwXttJlQbOdPOQVRv7DQ==
dependencies:
date-format "^2.1.0"
debug "^4.1.1"
fs-extra "^8.1.0"
strict-uri-encode@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
@ -9170,6 +9585,13 @@ tinycolor2@^1.1.2:
resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.1.tgz#f4fad333447bc0b07d4dc8e9209d8f39a8ac77e8"
integrity sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g=
tmp@0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14"
integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==
dependencies:
rimraf "^3.0.0"
to-absolute-glob@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz#1865f43d9e74b0822db9f145b78cff7d0f7c849b"
@ -9178,6 +9600,11 @@ to-absolute-glob@^2.0.0:
is-absolute "^1.0.0"
is-negated-glob "^1.0.0"
to-array@0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890"
integrity sha1-F+bBH3PdTz10zaek/zI46a2b+JA=
to-arraybuffer@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"
@ -9368,6 +9795,11 @@ typescript@^3.8.3:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.3.tgz#d3ac8883a97c26139e42df5e93eeece33d610b8a"
integrity sha512-D/wqnB2xzNFIcoBG9FG8cXRDjiqSTbG2wd8DMZeQyJlP1vfTkIxH4GKveWaEBYySKIg+USu+E+EDIR47SqnaMQ==
ua-parser-js@0.7.22:
version "0.7.22"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.22.tgz#960df60a5f911ea8f1c818f3747b99c6e177eae3"
integrity sha512-YUxzMjJ5T71w6a8WWVcMGM6YWOTX27rCoIQgLXiWaxqXSx9D7DNjiGWn1aJIRSQ5qr0xuhra77bSIh6voR/46Q==
unbzip2-stream@^1.0.9:
version "1.4.3"
resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7"
@ -9714,6 +10146,11 @@ vm-browserify@^1.0.1:
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==
void-elements@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=
watchpack-chokidar2@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.0.tgz#9948a1866cbbd6cb824dea13a7ed691f6c8ddff0"
@ -9761,6 +10198,17 @@ webpack-cli@^3.3.11:
v8-compile-cache "2.0.3"
yargs "13.2.4"
webpack-dev-middleware@^3.7.0:
version "3.7.3"
resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.3.tgz#0639372b143262e2b84ab95d3b91a7597061c2c5"
integrity sha512-djelc/zGiz9nZj/U7PTBi2ViorGJXEWo/3ltkPbDyxCXhhEXkW0ce99falaok4TPj+AsxLiXJR0EBOb0zh9fKQ==
dependencies:
memory-fs "^0.4.1"
mime "^2.4.4"
mkdirp "^0.5.1"
range-parser "^1.2.1"
webpack-log "^2.0.0"
webpack-dev-middleware@^3.7.2:
version "3.7.2"
resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.2.tgz#0019c3db716e3fa5cecbf64f2ab88a74bab331f3"
@ -9887,7 +10335,7 @@ which-module@^2.0.0:
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
which@^1.2.10, which@^1.2.14, which@^1.2.9, which@^1.3.1:
which@^1.2.1, which@^1.2.10, which@^1.2.14, which@^1.2.9, which@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
@ -9977,6 +10425,18 @@ ws@^6.2.1:
dependencies:
async-limiter "~1.0.0"
ws@^7.1.2:
version "7.4.1"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.1.tgz#a333be02696bd0e54cea0434e21dcc8a9ac294bb"
integrity sha512-pTsP8UAfhy3sk1lSk/O/s4tjD0CRwvMnzvwr4OKGX7ZvqZtUyx4KIJB5JWbkykPoc55tixMGgTNoh3k4FkNGFQ==
ws@~6.1.0:
version "6.1.4"
resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.4.tgz#5b5c8800afab925e94ccb29d153c8d02c1776ef9"
integrity sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==
dependencies:
async-limiter "~1.0.0"
xdg-basedir@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4"
@ -10015,6 +10475,11 @@ xmlbuilder@~11.0.0:
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3"
integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==
xmlhttprequest-ssl@~1.5.4:
version "1.5.5"
resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz#c2876b06168aadc40e57d97e81191ac8f4398b3e"
integrity sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=
xtend@^4.0.0, xtend@~4.0.0, xtend@~4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
@ -10056,7 +10521,7 @@ yargs-parser@^13.1.0, yargs-parser@^13.1.2:
camelcase "^5.0.0"
decamelize "^1.2.0"
yargs-parser@^18.1.1:
yargs-parser@^18.1.1, yargs-parser@^18.1.2:
version "18.1.3"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
@ -10122,6 +10587,23 @@ yargs@^15.1.0:
y18n "^4.0.0"
yargs-parser "^18.1.1"
yargs@^15.3.1:
version "15.4.1"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
dependencies:
cliui "^6.0.0"
decamelize "^1.2.0"
find-up "^4.1.0"
get-caller-file "^2.0.1"
require-directory "^2.1.1"
require-main-filename "^2.0.0"
set-blocking "^2.0.0"
string-width "^4.2.0"
which-module "^2.0.0"
y18n "^4.0.0"
yargs-parser "^18.1.2"
yargs@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-5.0.0.tgz#3355144977d05757dbb86d6e38ec056123b3a66e"
@ -10168,3 +10650,8 @@ yauzl@^2.10.0, yauzl@^2.4.2:
dependencies:
buffer-crc32 "~0.2.3"
fd-slicer "~1.1.0"
yeast@0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"
integrity sha1-AI4G2AlDIMNy28L47XagymyKxBk=