Skip to content

Commit

Permalink
feat(ember)!: dependency sources
Browse files Browse the repository at this point in the history
- adjust to backend
- adjust form to include the ability to set maintainers per source
  • Loading branch information
c0rydoras committed Mar 25, 2024
1 parent 666fac1 commit 8989e8b
Show file tree
Hide file tree
Showing 22 changed files with 281 additions and 144 deletions.
5 changes: 3 additions & 2 deletions api/outdated/outdated/tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ def test_fetch_end_of_life_invalid_json(mocker, requests_mock, project):
text="503 Service Temporarily Unavailable",
status_code=503,
)
LockfileParser([])._get_version(("django", "4.4.1"), provider="PIP") # noqa: SLF001
LockfileParser(project, [])._get_version(("django", "4.4.1"), provider="PIP") # noqa: SLF001


@pytest.mark.django_db()
Expand All @@ -227,6 +227,7 @@ def test_fetch_end_of_life_no_overwrite_if_already_set(
dependency_factory: DependencyFactory,
release_version_factory: ReleaseVersionFactory,
version_factory: VersionFactory,
project: models.Project,
) -> None:
end_of_life = date(2025, 1, 1)

Expand All @@ -248,7 +249,7 @@ def test_fetch_end_of_life_no_overwrite_if_already_set(

version: models.Version = version_factory(release_version=release_version)

LockfileParser([])._get_version( # noqa: SLF001
LockfileParser(project, [])._get_version( # noqa: SLF001
(dependency.name, version.version), provider=dependency.provider
)

Expand Down
2 changes: 1 addition & 1 deletion ember/app/components/project-compact.gjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ function icon(status) {
}

function version(project) {
return project.versionedDependencies[0];
return project.sources.at(0)?.versions.at(0);
}

const Dependency = <template>
Expand Down
67 changes: 36 additions & 31 deletions ember/app/components/project-detailed.gjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export default class ProjectDetailedComponent extends Component {
const project = await this.fetch.fetch(
`/api/projects/${this.args.project.id}/sync?${new URLSearchParams({
include:
'versionedDependencies,versionedDependencies.releaseVersion,versionedDependencies.releaseVersion.dependency',
'sources,sources.versions,sources.versions.release-version,sources.versions.release-version.dependency,sources.maintainers,sources.maintainers.user',
})}`,
{
method: 'POST',
Expand Down Expand Up @@ -60,8 +60,8 @@ export default class ProjectDetailedComponent extends Component {
}
});

get dependencyData() {
return this.args.project.versionedDependencies.map((version) => ({
sourceData(source) {
return source.versions.map((version) => ({
component: <template>
<tr class='{{statusToClass version.status}}'>{{yield}}</tr>
</template>,
Expand All @@ -76,21 +76,24 @@ export default class ProjectDetailedComponent extends Component {
}));
}

get maintainerData() {
return this.args.project.maintainers.map((m) => ({
values: {
username: <template>
{{#if m.isPrimary}}<UkIcon @icon='star' @ratio={{0.5}} />
{{/if}}{{m.user.username}}
</template>,
email: <template>
<a
class='uk-link-text'
href='mailto:{{m.user.email}}'
>{{m.user.email}}</a>
</template>,
},
}));
maintainerData(source) {
return source.maintainers
.slice()
.sort((a, b) => (a.isPrimary - b.isPrimary) * -1)
.map((m) => ({
values: {
username: <template>
{{#if m.isPrimary}}<UkIcon @icon='star' @ratio={{0.5}} />
{{/if}}{{m.user.username}}
</template>,
email: <template>
<a
class='uk-link-text'
href='mailto:{{m.user.email}}'
>{{m.user.email}}</a>
</template>,
},
}));
}

<template>
Expand All @@ -109,19 +112,21 @@ export default class ProjectDetailedComponent extends Component {
</div>

<hr class='seperator' />
<Table @data={{this.maintainerData}} @title='Maintainers' as |t|>
<t.head />
<t.body />
</Table>
<Table
@data={{this.dependencyData}}
@fallback='No dependencies yet'
@title='Dependencies'
as |t|
>
<t.head />
<t.body />
</Table>
{{#each @project.sources as |source|}}
<Table
@data={{this.sourceData source}}
@fallback='No dependencies yet'
@title={{source.path}}
as |t|
>
<t.head />
<t.body />
</Table>
<Table @data={{this.maintainerData source}} as |t|>
<t.head />
<t.body />
</Table>
{{/each}}
<UkButton
@label='Sync'
@loading={{this.syncProject.isRunning}}
Expand Down
52 changes: 7 additions & 45 deletions ember/app/components/project-form/component.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { service } from '@ember/service';
import Component from '@glimmer/component';
import { dropTask } from 'ember-concurrency';
import { scheduleTask } from 'ember-lifeline';
import { tracked } from 'tracked-built-ins';

import { emptyChangeset } from 'outdated/utils';
Expand All @@ -10,7 +9,6 @@ import ProjectValidations from 'outdated/validations/project';
export default class ProjectFormComponent extends Component {
@service store;
@service router;
@tracked maintainers;

@service notification;

Expand All @@ -19,51 +17,23 @@ export default class ProjectFormComponent extends Component {
this.args.project ?? this.store.createRecord('project'),
);

constructor(...args) {
super(...args);
if (this.args.project) {
scheduleTask(this, 'actions', () => {
this.maintainers = this.project.maintainers;
this.project.users = this.project.maintainers.map((m) => m.user);
this.project.primaryMaintainer = this.maintainers?.find(
(m) => m.isPrimary,
)?.user;
});
}
}

saveProject = dropTask(async () => {
try {
if (this.project.repoType === 'public') {
this.project.accessToken = '';
}

document
.querySelectorAll('[data-source-form-submit-button]')
.forEach((e) => e.click());

const project = await this.project.save({
adapterOptions: {
include:
'versionedDependencies,versionedDependencies.releaseVersion,versionedDependencies.releaseVersion.dependency,maintainers,maintainers.user',
'sources,sources.versions,sources.versions.release-version,sources.versions.release-version.dependency,sources.maintainers,sources.maintainers.user',
},
});

this.maintainers
?.filter((m) => !this.project.users?.includes(m.user))
.forEach((m) => m.destroyRecord());
this.project.users?.forEach((user) => {
const maintainer = this.maintainers?.find((m) => m.user.id === user.id);
if (maintainer) {
maintainer.isPrimary = user.id === this.primaryMaintainer.id;
if (maintainer.hasDirtyAttributes) maintainer.save();
} else {
this.store
.createRecord('maintainer', {
user,
project,
isPrimary: user === this.primaryMaintainer,
})
.save();
}
});

this.router.transitionTo('projects.detailed', project.id);
this.project.accessToken = '';
this.notification.success('Successfully saved!');
Expand All @@ -73,19 +43,11 @@ export default class ProjectFormComponent extends Component {
}
});

get primaryMaintainer() {
return (
this.project.users?.find(
(u) => u.id === this.project.primaryMaintainer?.id,
) ?? this.project.users[0]
);
get repoTypes() {
return ['public', 'access-token'];
}

get users() {
return this.store.peekAll('user');
}

get repoTypes() {
return ['public', 'access-token'];
}
}
24 changes: 3 additions & 21 deletions ember/app/components/project-form/template.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -30,26 +30,8 @@
autocomplete="off"
@hidden={{(eq this.project.repoType "public")}}
/>

<f.input
@name="users"
@type="select"
@label="Maintainers"
@multiple={{true}}
@value={{this.project.users}}
@options={{this.users}}
@searchField="searchField"
@visibleField="fullName"
/>
{{#if (gt this.project.users.length 1)}}
<f.input
@name="primaryMaintainer"
@type="select"
@options={{this.project.users}}
@value={{this.primaryMaintainer}}
@searchField="searchField"
@visibleField="fullName"
/>
{{/if}}
{{#each @project.sources as |source|}}
<SourceForm @source={{source}} />
{{/each}}
<f.button @loading={{this.saveProject.isRunning}} />
</Form>
106 changes: 106 additions & 0 deletions ember/app/components/source-form.gjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { service } from '@ember/service';
import Component from '@glimmer/component';
import { dropTask } from 'ember-concurrency';
import perform from 'ember-concurrency/helpers/perform';
import { scheduleTask } from 'ember-lifeline';
import { lt, or } from 'ember-truth-helpers';
import { tracked } from 'tracked-built-ins';

import Form from './form';

import { emptyChangeset } from 'outdated/utils';
import SourceValidations from 'outdated/validations/source';

export default class SourceForm extends Component {
@service store;
@service notification;

@tracked source = emptyChangeset(SourceValidations, this.args.source);

constructor(...args) {
super(...args);

scheduleTask(this, 'actions', () => {
const source = this.args.source;
source.users = source.maintainers.map((m) => m.user);
source.primaryMaintainer = source.maintainers.find(
(m) => m.isPrimary,
)?.user;
});
}

get primaryMaintainer() {
return (
this.source.users?.find(
(u) => u.id === this.source.primaryMaintainer?.id,
) ?? this.source.users[0]
);
}

get users() {
return this.store.peekAll('user');
}

saveMaintainers = dropTask(async () => {
try {
this.source.maintainers
?.filter(
(m) => !this.source.users?.map((u) => u.id).includes(m.user.id),
)
.forEach((m) => m.destroyRecord());
this.source.users
?.filter(
(u) => !this.source.maintainers?.find((m) => m.user.id === u.id),
)
.forEach((user) => {
this.store.createRecord('maintainer', {
user,
source: this.args.source,
isPrimary: user.id === this.primaryMaintainer.id,
});
});
this.source.maintainers.forEach((m) => {
m.isPrimary = m.user.id === this.primaryMaintainer.id;
if (m.hasDirtyAttributes) m.save();
});
this.store.findRecord('dependency-source', this.source.id, {
include: 'maintainers,maintainers.user',
});
this.notification.success(
`Successfully saved maintainers for ${this.source.path}`,
);
} catch (e) {
this.notification.danger(e);
console.error(e);
}
});

<template>
<Form
@onSubmit={{perform this.saveMaintainers}}
@model={{this.source}}
@name={{@source.path}}
as |f|
>
<f.input
@label='Maintainers'
@name='users'
@type='select'
@options={{this.users}}
@multiple={{true}}
@searchField='searchField'
@visibleField='fullName'
/>
<f.input
@name='primaryMaintainer'
@options={{this.source.users}}
@hidden={{lt (or this.source.users.length 0) 2}}
@value={{this.primaryMaintainer}}
@type='select'
@searchField='searchField'
@visibleField='fullName'
/>
<f.button class='uk-hidden' data-source-form-submit-button />
</Form>
</template>
}
20 changes: 20 additions & 0 deletions ember/app/models/dependency-source.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Model, { attr, hasMany, belongsTo } from '@ember-data/model';
import { tracked } from 'tracked-built-ins';

export default class DependencySourceModel extends Model {
@attr status;
@attr path;

@hasMany('version', { inverse: null, async: false }) versions;
@hasMany('maintainer', { inverse: 'source', async: false, as: 'source' })
maintainers;
@belongsTo('project', {
inverse: 'sources',
async: true,
as: 'dependency-source',
})
project;

@tracked users;
@tracked primaryMaintainer;
}
8 changes: 7 additions & 1 deletion ember/app/models/maintainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ import Model, { attr, belongsTo } from '@ember-data/model';

export default class MaintainerModel extends Model {
@attr isPrimary;
@belongsTo('project', { inverse: 'maintainers', async: true }) project;
@belongsTo('source', {
inverse: 'maintainers',
async: true,
as: 'maintainer',
polymorphic: true,
})
source;
@belongsTo('user', { inverse: null, async: false }) user;
}
Loading

0 comments on commit 8989e8b

Please sign in to comment.