-
-
Notifications
You must be signed in to change notification settings - Fork 838
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: package manager improvements (#3943)
* chore: track * Apply fixes from StyleCI * chore: track * Apply fixes from StyleCI * fix: installing beta packages #3792 * chore: guess package not found error * Apply fixes from StyleCI * feat: queue improvements * feat: queue improvements * fix: issues with job failure and unique runs * fix: enforce one composer action at a time * feat: add cause to queued command output * Apply fixes from StyleCI * feat: add soft & hard extension update options * chore: explain why an extension cannot be removed * chore: remove test * chore: simplify * docs: readme * Apply fixes from StyleCI * fea: allow adding repositories and auth methods * chore: prevent future issues when min stability is set * chore: typings check * fix: phpstan * Apply fixes from StyleCI * fix: bugs
- Loading branch information
Showing
54 changed files
with
1,390 additions
and
276 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,18 @@ | ||
# Package Manager | ||
|
||
*An Experiment.* | ||
The package manager is a tool that allows you to easily install and manage extensions. It runs [composer](https://getcomposer.org/) under the hood. | ||
|
||
Read: https://github.com/flarum/package-manager/wiki | ||
## Security | ||
|
||
If admin access is given to untrustworthy users, they can install malicious extensions. Please be careful. | ||
|
||
This extension is optional and can be removed for those who prefer to manually manage installs and updates through the command line interface. | ||
|
||
## Troubleshooting | ||
|
||
If you have many extensions installed, you may run into memory issues when using the package manager. If this happens, you can use an asynchronous queue that will run the package manager in the background. | ||
|
||
* Simple database queue guide: https://discuss.flarum.org/d/28151-database-queue-the-simplest-queue-even-for-shared-hosting | ||
* (Advanced) Redis queue: https://discuss.flarum.org/d/21873-redis-sessions-cache-queues | ||
|
||
You can find detailed logs on the package manager operations in the `storage/logs/composer` directory. Please include the latest log file when reporting issues in the [Flarum support forum](https://discuss.flarum.org/t/support). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
88 changes: 88 additions & 0 deletions
88
extensions/package-manager/js/src/admin/components/AuthMethodModal.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
import Modal, { IInternalModalAttrs } from 'flarum/common/components/Modal'; | ||
import Mithril from 'mithril'; | ||
import app from 'flarum/admin/app'; | ||
import Select from 'flarum/common/components/Select'; | ||
import Stream from 'flarum/common/utils/Stream'; | ||
import Button from 'flarum/common/components/Button'; | ||
import extractText from 'flarum/common/utils/extractText'; | ||
|
||
export interface IAuthMethodModalAttrs extends IInternalModalAttrs { | ||
onsubmit: (type: string, host: string, token: string) => void; | ||
type?: string; | ||
host?: string; | ||
token?: string; | ||
} | ||
|
||
export default class AuthMethodModal<CustomAttrs extends IAuthMethodModalAttrs = IAuthMethodModalAttrs> extends Modal<CustomAttrs> { | ||
protected type!: Stream<string>; | ||
protected host!: Stream<string>; | ||
protected token!: Stream<string>; | ||
|
||
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) { | ||
super.oninit(vnode); | ||
|
||
this.type = Stream(this.attrs.type || 'bearer'); | ||
this.host = Stream(this.attrs.host || ''); | ||
this.token = Stream(this.attrs.token || ''); | ||
} | ||
|
||
className(): string { | ||
return 'AuthMethodModal Modal--small'; | ||
} | ||
|
||
title(): Mithril.Children { | ||
const context = this.attrs.host ? 'edit' : 'add'; | ||
return app.translator.trans(`flarum-package-manager.admin.auth_config.${context}_label`); | ||
} | ||
|
||
content(): Mithril.Children { | ||
const types = { | ||
'github-oauth': app.translator.trans('flarum-package-manager.admin.auth_config.types.github-oauth'), | ||
'gitlab-oauth': app.translator.trans('flarum-package-manager.admin.auth_config.types.gitlab-oauth'), | ||
'gitlab-token': app.translator.trans('flarum-package-manager.admin.auth_config.types.gitlab-token'), | ||
bearer: app.translator.trans('flarum-package-manager.admin.auth_config.types.bearer'), | ||
}; | ||
|
||
return ( | ||
<div className="Modal-body"> | ||
<div className="Form-group"> | ||
<label>{app.translator.trans('flarum-package-manager.admin.auth_config.add_modal.type_label')}</label> | ||
<Select options={types} value={this.type()} onchange={this.type} /> | ||
</div> | ||
<div className="Form-group"> | ||
<label>{app.translator.trans('flarum-package-manager.admin.auth_config.add_modal.host_label')}</label> | ||
<input | ||
className="FormControl" | ||
bidi={this.host} | ||
placeholder={app.translator.trans('flarum-package-manager.admin.auth_config.add_modal.host_placeholder')} | ||
/> | ||
</div> | ||
<div className="Form-group"> | ||
<label>{app.translator.trans('flarum-package-manager.admin.auth_config.add_modal.token_label')}</label> | ||
<textarea | ||
className="FormControl" | ||
oninput={(e: InputEvent) => this.token((e.target as HTMLTextAreaElement).value)} | ||
rows="6" | ||
placeholder={ | ||
this.token() === '***' | ||
? extractText(app.translator.trans('flarum-package-manager.admin.auth_config.add_modal.unchanged_token_placeholder')) | ||
: '' | ||
} | ||
> | ||
{this.token() === '***' ? '' : this.token()} | ||
</textarea> | ||
</div> | ||
<div className="Form-group"> | ||
<Button className="Button Button--primary" onclick={this.submit.bind(this)}> | ||
{app.translator.trans('flarum-package-manager.admin.auth_config.add_modal.submit_button')} | ||
</Button> | ||
</div> | ||
</div> | ||
); | ||
} | ||
|
||
submit() { | ||
this.attrs.onsubmit(this.type(), this.host(), this.token()); | ||
this.hide(); | ||
} | ||
} |
105 changes: 105 additions & 0 deletions
105
extensions/package-manager/js/src/admin/components/ConfigureAuth.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
import app from 'flarum/admin/app'; | ||
import type Mithril from 'mithril'; | ||
import ConfigureJson, { IConfigureJson } from './ConfigureJson'; | ||
import Button from 'flarum/common/components/Button'; | ||
import AuthMethodModal from './AuthMethodModal'; | ||
import extractText from 'flarum/common/utils/extractText'; | ||
|
||
export default class ConfigureAuth extends ConfigureJson<IConfigureJson> { | ||
protected type = 'auth'; | ||
|
||
title(): Mithril.Children { | ||
return app.translator.trans('flarum-package-manager.admin.auth_config.title'); | ||
} | ||
|
||
className(): string { | ||
return 'ConfigureAuth'; | ||
} | ||
|
||
content(): Mithril.Children { | ||
const authSettings = Object.keys(this.settings); | ||
|
||
return ( | ||
<div className="SettingsGroups-content"> | ||
{authSettings.length ? ( | ||
authSettings.map((type) => { | ||
const hosts = this.settings[type](); | ||
|
||
return ( | ||
<div className="Form-group"> | ||
<label>{app.translator.trans(`flarum-package-manager.admin.auth_config.types.${type}`)}</label> | ||
<div className="ConfigureAuth-hosts"> | ||
{Object.keys(hosts).map((host) => { | ||
const data = hosts[host] as string | Record<string, string>; | ||
|
||
return ( | ||
<div className="ButtonGroup ButtonGroup--full"> | ||
<Button | ||
className="Button" | ||
icon="fas fa-key" | ||
onclick={() => | ||
app.modal.show(AuthMethodModal, { | ||
type, | ||
host, | ||
token: data, | ||
onsubmit: this.onchange.bind(this), | ||
}) | ||
} | ||
> | ||
{host} | ||
</Button> | ||
<Button | ||
className="Button Button--icon" | ||
icon="fas fa-trash" | ||
aria-label={app.translator.trans('flarum-package-manager.admin.auth_config.delete_label')} | ||
onclick={() => { | ||
if (confirm(extractText(app.translator.trans('flarum-package-manager.admin.auth_config.delete_confirmation')))) { | ||
const newType = { ...this.setting(type)() }; | ||
delete newType[host]; | ||
|
||
if (Object.keys(newType).length) { | ||
this.setting(type)(newType); | ||
} else { | ||
delete this.settings[type]; | ||
} | ||
} | ||
}} | ||
/> | ||
</div> | ||
); | ||
})} | ||
</div> | ||
</div> | ||
); | ||
}) | ||
) : ( | ||
<span className="helpText">{app.translator.trans('flarum-package-manager.admin.auth_config.no_auth_methods_configured')}</span> | ||
)} | ||
</div> | ||
); | ||
} | ||
|
||
submitButton(): Mithril.Children[] { | ||
const items = super.submitButton(); | ||
|
||
items.push( | ||
<Button | ||
className="Button" | ||
loading={this.loading} | ||
onclick={() => | ||
app.modal.show(AuthMethodModal, { | ||
onsubmit: this.onchange.bind(this), | ||
}) | ||
} | ||
> | ||
{app.translator.trans('flarum-package-manager.admin.auth_config.add_label')} | ||
</Button> | ||
); | ||
|
||
return items; | ||
} | ||
|
||
onchange(type: string, host: string, token: string) { | ||
this.setting(type)({ ...this.setting(type)(), [host]: token }); | ||
} | ||
} |
108 changes: 108 additions & 0 deletions
108
extensions/package-manager/js/src/admin/components/ConfigureComposer.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
import app from 'flarum/admin/app'; | ||
import type Mithril from 'mithril'; | ||
import ConfigureJson, { type IConfigureJson } from './ConfigureJson'; | ||
import Button from 'flarum/common/components/Button'; | ||
import extractText from 'flarum/common/utils/extractText'; | ||
import RepositoryModal from './RepositoryModal'; | ||
|
||
export type Repository = { | ||
type: 'composer' | 'vcs' | 'path'; | ||
url: string; | ||
}; | ||
|
||
export default class ConfigureComposer extends ConfigureJson<IConfigureJson> { | ||
protected type = 'composer'; | ||
|
||
title(): Mithril.Children { | ||
return app.translator.trans('flarum-package-manager.admin.composer.title'); | ||
} | ||
|
||
className(): string { | ||
return 'ConfigureComposer'; | ||
} | ||
|
||
content(): Mithril.Children { | ||
return ( | ||
<div className="SettingsGroups-content"> | ||
{this.attrs.buildSettingComponent.call(this, { | ||
setting: 'minimum-stability', | ||
label: app.translator.trans('flarum-package-manager.admin.composer.minimum_stability.label'), | ||
help: app.translator.trans('flarum-package-manager.admin.composer.minimum_stability.help'), | ||
type: 'select', | ||
options: { | ||
stable: app.translator.trans('flarum-package-manager.admin.composer.minimum_stability.options.stable'), | ||
RC: app.translator.trans('flarum-package-manager.admin.composer.minimum_stability.options.rc'), | ||
beta: app.translator.trans('flarum-package-manager.admin.composer.minimum_stability.options.beta'), | ||
alpha: app.translator.trans('flarum-package-manager.admin.composer.minimum_stability.options.alpha'), | ||
dev: app.translator.trans('flarum-package-manager.admin.composer.minimum_stability.options.dev'), | ||
}, | ||
})} | ||
<div className="Form-group"> | ||
<label>{app.translator.trans('flarum-package-manager.admin.composer.repositories.label')}</label> | ||
<div className="helpText">{app.translator.trans('flarum-package-manager.admin.composer.repositories.help')}</div> | ||
<div className="ConfigureComposer-repositories"> | ||
{Object.keys(this.setting('repositories')() || {}).map((name) => { | ||
const repository = this.setting('repositories')()[name] as Repository; | ||
|
||
return ( | ||
<div className="ButtonGroup ButtonGroup--full"> | ||
<Button | ||
className="Button" | ||
icon={ | ||
{ | ||
composer: 'fas fa-cubes', | ||
vcs: 'fas fa-code-branch', | ||
path: 'fas fa-folder', | ||
}[repository.type] | ||
} | ||
onclick={() => | ||
app.modal.show(RepositoryModal, { | ||
name, | ||
repository, | ||
onsubmit: this.onchange.bind(this), | ||
}) | ||
} | ||
> | ||
{name} ({repository.type}) | ||
</Button> | ||
<Button | ||
className="Button Button--icon" | ||
icon="fas fa-trash" | ||
aria-label={app.translator.trans('flarum-package-manager.admin.composer.delete_repository_label')} | ||
onclick={() => { | ||
if (confirm(extractText(app.translator.trans('flarum-package-manager.admin.composer.delete_repository_confirmation')))) { | ||
const repositories = { ...this.setting('repositories')() }; | ||
delete repositories[name]; | ||
|
||
this.setting('repositories')(repositories); | ||
} | ||
}} | ||
/> | ||
</div> | ||
); | ||
})} | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
} | ||
|
||
submitButton(): Mithril.Children[] { | ||
const items = super.submitButton(); | ||
|
||
items.push( | ||
<Button className="Button" onclick={() => app.modal.show(RepositoryModal, { onsubmit: this.onchange.bind(this) })}> | ||
{app.translator.trans('flarum-package-manager.admin.composer.add_repository_label')} | ||
</Button> | ||
); | ||
|
||
return items; | ||
} | ||
|
||
onchange(repository: Repository, name: string) { | ||
this.setting('repositories')({ | ||
...this.setting('repositories')(), | ||
[name]: repository, | ||
}); | ||
} | ||
} |
Oops, something went wrong.