2022-08-22 19:46:25 +00:00
|
|
|
|
import { basicButton, textButton } from 'app/client/ui2018/buttons';
|
2022-09-06 01:51:57 +00:00
|
|
|
|
import { theme, vars } from 'app/client/ui2018/cssVars';
|
2022-08-22 19:46:25 +00:00
|
|
|
|
import { icon } from 'app/client/ui2018/icons';
|
2020-10-02 15:10:00 +00:00
|
|
|
|
import { confirmModal } from 'app/client/ui2018/modals';
|
2022-08-22 19:46:25 +00:00
|
|
|
|
import { Disposable, dom, IDomArgs, makeTestId, Observable, observable, styled } from 'grainjs';
|
2020-10-02 15:10:00 +00:00
|
|
|
|
|
|
|
|
|
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.
|
2022-01-24 20:38:32 +00:00
|
|
|
|
inputArgs?: IDomArgs<HTMLInputElement>;
|
2020-10-02 15:10:00 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
2022-02-19 09:46:49 +00:00
|
|
|
|
* that triggers the `options.onCreate` callback. It is the responsibility of the caller to update
|
2020-10-02 15:10:00 +00:00
|
|
|
|
* 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;
|
2022-01-24 20:38:32 +00:00
|
|
|
|
private _inputArgs: IDomArgs<HTMLInputElement>;
|
2020-10-02 15:10:00 +00:00
|
|
|
|
private _loading = observable(false);
|
2022-02-21 23:44:29 +00:00
|
|
|
|
private _isHidden: Observable<boolean> = Observable.create(this, true);
|
2020-10-02 15:10:00 +00:00
|
|
|
|
|
|
|
|
|
constructor(options: IWidgetOptions) {
|
|
|
|
|
super();
|
|
|
|
|
this._apiKey = options.apiKey;
|
|
|
|
|
this._onDeleteCB = options.onDelete;
|
|
|
|
|
this._onCreateCB = options.onCreate;
|
|
|
|
|
this._anonymous = Boolean(options.anonymous);
|
2022-01-24 20:38:32 +00:00
|
|
|
|
this._inputArgs = options.inputArgs ?? [];
|
2020-10-02 15:10:00 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public buildDom() {
|
|
|
|
|
return dom('div', testId('container'), dom.style('position', 'relative'),
|
|
|
|
|
dom.maybe(this._apiKey, (apiKey) => dom('div',
|
|
|
|
|
cssRow(
|
|
|
|
|
cssInput(
|
2022-01-24 20:38:32 +00:00
|
|
|
|
{
|
|
|
|
|
readonly: true,
|
|
|
|
|
value: this._apiKey.get(),
|
|
|
|
|
},
|
2022-02-21 23:44:29 +00:00
|
|
|
|
dom.attr('type', (use) => use(this._isHidden) ? 'password' : 'text'),
|
2022-01-24 20:38:32 +00:00
|
|
|
|
testId('key'),
|
2022-02-21 23:44:29 +00:00
|
|
|
|
{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); }
|
|
|
|
|
}),
|
2022-01-24 20:38:32 +00:00
|
|
|
|
this._inputArgs
|
2020-10-02 15:10:00 +00:00
|
|
|
|
),
|
|
|
|
|
cssTextBtn(
|
2022-08-22 19:46:25 +00:00
|
|
|
|
cssTextBtnIcon('Remove'), 'Remove',
|
2020-10-02 15:10:00 +00:00
|
|
|
|
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. '
|
|
|
|
|
+ 'Don’t 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', `
|
2022-09-06 01:51:57 +00:00
|
|
|
|
margin-top: 8px;
|
|
|
|
|
color: ${theme.lightText};
|
|
|
|
|
font-size: ${vars.mediumFontSize};
|
2020-10-02 15:10:00 +00:00
|
|
|
|
`);
|
|
|
|
|
|
|
|
|
|
const cssInput = styled('input', `
|
2022-09-06 01:51:57 +00:00
|
|
|
|
background-color: transparent;
|
|
|
|
|
color: ${theme.inputFg};
|
|
|
|
|
border: 1px solid ${theme.inputBorder};
|
|
|
|
|
padding: 4px;
|
|
|
|
|
border-radius: 3px;
|
2020-10-02 15:10:00 +00:00
|
|
|
|
outline: none;
|
|
|
|
|
flex: 1 0 0;
|
|
|
|
|
`);
|
|
|
|
|
|
|
|
|
|
const cssRow = styled('div', `
|
|
|
|
|
display: flex;
|
|
|
|
|
`);
|
|
|
|
|
|
2022-08-22 19:46:25 +00:00
|
|
|
|
const cssTextBtn = styled(textButton, `
|
|
|
|
|
text-align: left;
|
2020-10-02 15:10:00 +00:00
|
|
|
|
width: 90px;
|
|
|
|
|
margin-left: 16px;
|
|
|
|
|
`);
|
|
|
|
|
|
2022-08-22 19:46:25 +00:00
|
|
|
|
const cssTextBtnIcon = styled(icon, `
|
|
|
|
|
margin: 0 4px 2px 0;
|
|
|
|
|
`);
|