gristlabs_grist-core/app/client/ui/ApiKey.ts
George Gevoian 36843e632b (core) Hide API key when it's not selected
Summary:
The API key is now hidden by default. Clicking the input
will select and reveal the key. As soon as the key loses
selection, it is hidden again.

Test Plan: Project test.

Reviewers: jarek

Reviewed By: jarek

Subscribers: jarek

Differential Revision: https://phab.getgrist.com/D3270
2022-02-21 17:26:45 -08:00

142 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import * as billingPageCss from 'app/client/ui/BillingPageCss';
import { basicButton } from 'app/client/ui2018/buttons';
import { confirmModal } from 'app/client/ui2018/modals';
import { Disposable, dom, IDomArgs, makeTestId, Observable, observable, styled } from "grainjs";
interface IWidgetOptions {
apiKey: Observable<string>;
onDelete: () => Promise<void>;
onCreate: () => Promise<void>;
anonymous?: boolean; // Configure appearance and available options for anonymous use.
// When anonymous, no modifications are permitted to profile information.
// TODO: add browser test for this option.
inputArgs?: IDomArgs<HTMLInputElement>;
}
const testId = makeTestId('test-apikey-');
/**
* ApiKey component shows an api key with controls to change it. Expects `options.apiKey` the api
* key and shows it if value is truthy along with a 'Delete' button that triggers the
* `options.onDelete` callback. When `options.apiKey` is falsy, hides it and show a 'Create' button
* that triggers the `options.onCreate` callback. It is the responsibility of the caller to update
* the `options.apiKey` to its new value.
*/
export class ApiKey extends Disposable {
private _apiKey: Observable<string>;
private _onDeleteCB: () => Promise<void>;
private _onCreateCB: () => Promise<void>;
private _anonymous: boolean;
private _inputArgs: IDomArgs<HTMLInputElement>;
private _loading = observable(false);
private _isHidden: Observable<boolean> = Observable.create(this, true);
constructor(options: IWidgetOptions) {
super();
this._apiKey = options.apiKey;
this._onDeleteCB = options.onDelete;
this._onCreateCB = options.onCreate;
this._anonymous = Boolean(options.anonymous);
this._inputArgs = options.inputArgs ?? [];
}
public buildDom() {
return dom('div', testId('container'), dom.style('position', 'relative'),
dom.maybe(this._apiKey, (apiKey) => dom('div',
cssRow(
cssInput(
{
readonly: true,
value: this._apiKey.get(),
},
dom.attr('type', (use) => use(this._isHidden) ? 'password' : 'text'),
testId('key'),
{title: 'Click to show'},
dom.on('click', (_ev, el) => {
this._isHidden.set(false);
setTimeout(() => el.select(), 0);
}),
dom.on('blur', (ev) => {
// Hide the key when it is no longer selected.
if (ev.target !== document.activeElement) { this._isHidden.set(true); }
}),
this._inputArgs
),
cssTextBtn(
cssBillingIcon('Remove'), 'Remove',
dom.on('click', () => this._showRemoveKeyModal()),
testId('delete'),
dom.boolAttr('disabled', (use) => use(this._loading) || this._anonymous)
),
),
description(this._getDescription(), testId('description')),
)),
dom.maybe((use) => !(use(this._apiKey) || this._anonymous), () => [
basicButton('Create', dom.on('click', () => this._onCreate()), testId('create'),
dom.boolAttr('disabled', this._loading)),
description('By generating an API key, you will be able to make API calls '
+ 'for your own account.', testId('description')),
]),
);
}
// Switch the `_loading` flag to `true` and later, once promise resolves, switch it back to
// `false`.
private async _switchLoadingFlag(promise: Promise<any>) {
this._loading.set(true);
try {
await promise;
} finally {
this._loading.set(false);
}
}
private _onDelete(): Promise<void> {
return this._switchLoadingFlag(this._onDeleteCB());
}
private _onCreate(): Promise<void> {
return this._switchLoadingFlag(this._onCreateCB());
}
private _getDescription(): string {
if (!this._anonymous) {
return 'This API key can be used to access your account via the API. '
+ 'Dont share your API key with anyone.';
} else {
return 'This API key can be used to access this account anonymously via the API.';
}
}
private _showRemoveKeyModal(): void {
confirmModal(
`Remove API Key`, 'Remove',
() => this._onDelete(),
`You're about to delete an API key. This will cause all future ` +
`requests using this API key to be rejected. Do you still want to delete?`
);
}
}
const description = styled('div', `
color: #8a8a8a;
font-size: 13px;
`);
const cssInput = styled('input', `
outline: none;
flex: 1 0 0;
`);
const cssRow = styled('div', `
display: flex;
`);
const cssTextBtn = styled(billingPageCss.billingTextBtn, `
width: 90px;
margin-left: 16px;
`);
const cssBillingIcon = billingPageCss.billingIcon;