mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
fix(project): show production, staging, and development environments first
Closes #12317 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
35e1a05705
commit
0f0a1e920d
@@ -64,7 +64,7 @@ class Show extends Component
|
||||
$canCreateResource = auth()->user()->can('createAnyResource');
|
||||
|
||||
return view('livewire.project.show', [
|
||||
'environmentsJs' => $this->project->environments->map(function (Environment $environment) use ($canCreateResource, $canUpdateProject): array {
|
||||
'environmentsJs' => $this->project->environments->sort(fn (Environment $first, Environment $second): int => Environment::compareNames($first->name, $second->name))->values()->map(function (Environment $environment) use ($canCreateResource, $canUpdateProject): array {
|
||||
$resourceCount = collect([
|
||||
$environment->applications_count,
|
||||
$environment->services_count,
|
||||
|
||||
@@ -56,6 +56,18 @@ class Environment extends BaseModel
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort order of environment names: production, staging, and development first, then the others by name.
|
||||
* resources/js/environment-order.js has the same order for the canvases.
|
||||
*/
|
||||
public static function compareNames(string $first, string $second): int
|
||||
{
|
||||
$order = ['production', 'staging', 'development'];
|
||||
$rank = fn (string $name): int => ($index = array_search(strtolower($name), $order, true)) === false ? count($order) : $index;
|
||||
|
||||
return $rank($first) <=> $rank($second) ?: strnatcasecmp($first, $second);
|
||||
}
|
||||
|
||||
public static function ownedByCurrentTeam()
|
||||
{
|
||||
return Environment::whereRelation('project.team', 'id', currentTeam()->id)->orderBy('name');
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
// DOM-free: `node --test` imports this file.
|
||||
|
||||
import { BOX_GAP, freeSpot, MAX_ZOOM, MIN_ZOOM } from './canvas-core.js';
|
||||
import { compareEnvironmentNames } from './environment-order.js';
|
||||
|
||||
export const CARD_WIDTH = 260;
|
||||
export const COLUMN_GAP = 48;
|
||||
@@ -130,7 +131,12 @@ const contextGroups = nodes => {
|
||||
}
|
||||
// Server view: the unmanaged containers group comes first, so it is not lost below many projects.
|
||||
const unmanaged = ([, members]) => (members[0]?.type === 'container' ? 0 : 1);
|
||||
return [...groups.entries()].sort((a, b) => unmanaged(a) - unmanaged(b) || a[0].localeCompare(b[0]));
|
||||
// Then by project, then production, staging, and development before the other environments.
|
||||
const project = ([name, members]) => members[0]?.project?.name ?? name;
|
||||
const environment = ([name, members]) => members[0]?.environment?.name ?? name;
|
||||
return [...groups.entries()].sort(
|
||||
(a, b) => unmanaged(a) - unmanaged(b) || project(a).localeCompare(project(b)) || compareEnvironmentNames(environment(a), environment(b)),
|
||||
);
|
||||
};
|
||||
const byTypeAndName = (a, b) => columnOf(a) - columnOf(b) || String(a.name).localeCompare(String(b.name));
|
||||
|
||||
@@ -637,7 +643,7 @@ export function statusLabel(status) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect and server views: projects of the resources, each with its environments, sorted by name.
|
||||
* Connect and server views: projects of the resources sorted by name, each with its environments in environment order.
|
||||
* @returns {{uuid: string, name: string, environments: {uuid: string, name: string}[]}[]}
|
||||
*/
|
||||
export function filterOptions(graph) {
|
||||
@@ -649,7 +655,7 @@ export function filterOptions(graph) {
|
||||
}
|
||||
const byName = (a, b) => String(a.name).localeCompare(String(b.name));
|
||||
return [...projects.values()]
|
||||
.map(p => ({ uuid: p.uuid, name: p.name, environments: [...p.environments.values()].sort(byName) }))
|
||||
.map(p => ({ uuid: p.uuid, name: p.name, environments: [...p.environments.values()].sort((a, b) => compareEnvironmentNames(a.name, b.name)) }))
|
||||
.sort(byName);
|
||||
}
|
||||
|
||||
|
||||
@@ -258,6 +258,20 @@ test('server view: one frame per project / environment, and the frames do not ov
|
||||
assert.equal(positions.a.y, positions.b.y);
|
||||
});
|
||||
|
||||
test('groups: production, staging, and development come before the other environments of a project', () => {
|
||||
const env = (id, project, environment) =>
|
||||
srv(id, 'application', `${project} / ${environment}`, { project: { uuid: project, name: project }, environment: { uuid: id, name: environment } });
|
||||
const graph = {
|
||||
layout: 'server',
|
||||
zones: [{ id: 'z1', name: 'web' }],
|
||||
nodes: [env('a', 'Shop', 'deprecated'), env('b', 'Shop', 'development'), env('c', 'Shop', 'production'), env('d', 'Shop', 'qa'), env('e', 'Shop', 'staging'), env('f', 'Blog', 'zeta')],
|
||||
links: [],
|
||||
positions: {},
|
||||
};
|
||||
const frames = groupFrames(graph.nodes, layoutGraph(graph));
|
||||
assert.deepEqual(frames.map(f => f.name), ['Blog / zeta', 'Shop / production', 'Shop / staging', 'Shop / development', 'Shop / deprecated', 'Shop / qa']);
|
||||
});
|
||||
|
||||
const envNode = (id, type, zone, environment, extra = {}) => srv(id, type, `Shop / ${environment}`, { zone, project: { uuid: 'p' }, environment: { uuid: environment }, ...extra });
|
||||
|
||||
test('server view: cards link only to cards of the same environment', () => {
|
||||
@@ -498,15 +512,16 @@ test('variables view: filter resources by project and environment, keep every va
|
||||
node('variable:1', 'variable', 'sources'),
|
||||
node('a', 'application', 'resources', { project: shop, environment: { uuid: 'e1', name: 'production' } }),
|
||||
node('b', 'application', 'resources', { project: shop, environment: { uuid: 'e2', name: 'staging' } }),
|
||||
node('d', 'application', 'resources', { project: shop, environment: { uuid: 'e4', name: 'deprecated' } }),
|
||||
node('c', 'application', 'resources', { project: blog, environment: { uuid: 'e3', name: 'production' } }),
|
||||
],
|
||||
links: [{ id: 'l1', source: 'variable:1', target: 'a' }, { id: 'l2', source: 'variable:1', target: 'c' }],
|
||||
};
|
||||
|
||||
assert.deepEqual(filterOptions(g).map(p => [p.name, p.environments.map(e => e.name)]), [['Blog', ['production']], ['Shop', ['production', 'staging']]]);
|
||||
assert.deepEqual(filterOptions(g).map(p => [p.name, p.environments.map(e => e.name)]), [['Blog', ['production']], ['Shop', ['production', 'staging', 'deprecated']]]);
|
||||
assert.equal(filterGraph(g, {}), g);
|
||||
const shopOnly = filterGraph(g, { project: 'p1' });
|
||||
assert.deepEqual(shopOnly.nodes.map(n => n.id), ['variable:1', 'a', 'b']);
|
||||
assert.deepEqual(shopOnly.nodes.map(n => n.id), ['variable:1', 'a', 'b', 'd']);
|
||||
assert.deepEqual(shopOnly.links.map(l => l.id), ['l1']);
|
||||
assert.deepEqual(filterGraph(g, { environment: 'e2' }).nodes.map(n => n.id), ['variable:1', 'b']);
|
||||
assert.deepEqual(filterGraph(g, { environment: 'e2' }).links, []);
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
const ORDER = ['production', 'staging', 'development'];
|
||||
|
||||
const rank = name => {
|
||||
const index = ORDER.indexOf(String(name).toLowerCase());
|
||||
return index === -1 ? ORDER.length : index;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sort order of environment names: production, staging, and development first, then the others by name.
|
||||
* Environment::compareNames() has the same order for the project page.
|
||||
*/
|
||||
export const compareEnvironmentNames = (a, b) => rank(a) - rank(b) || String(a).localeCompare(String(b));
|
||||
@@ -211,13 +211,17 @@
|
||||
function projectEnvironments() {
|
||||
return {
|
||||
search: '',
|
||||
sortBy: 'name-asc',
|
||||
sortBy: 'default',
|
||||
sortOpen: false,
|
||||
viewMode: @js($this->listView('project', 'grid')),
|
||||
page: 1,
|
||||
pageSize: 12,
|
||||
environments: @js($environmentsJs),
|
||||
sortOptions: [{
|
||||
value: 'default',
|
||||
label: 'Default'
|
||||
},
|
||||
{
|
||||
value: 'name-asc',
|
||||
label: 'Name A–Z'
|
||||
},
|
||||
@@ -249,8 +253,12 @@
|
||||
return second.resourceCount - first.resourceCount ||
|
||||
first.name.localeCompare(second.name);
|
||||
}
|
||||
if (this.sortBy === 'name-asc') {
|
||||
return first.name.localeCompare(second.name);
|
||||
}
|
||||
|
||||
return first.name.localeCompare(second.name);
|
||||
// Default: the server order (production, staging, development, then by name).
|
||||
return 0;
|
||||
});
|
||||
},
|
||||
get totalPages() {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Project\Show as ProjectShow;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Project;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
test('project page lists production, staging, and development before the other environments', function () {
|
||||
Queue::fake();
|
||||
InstanceSettings::unguarded(fn () => InstanceSettings::query()->create(['id' => 0]));
|
||||
$user = User::factory()->create();
|
||||
$team = Team::factory()->create();
|
||||
$user->teams()->attach($team, ['role' => 'owner']);
|
||||
$this->actingAs($user);
|
||||
session(['currentTeam' => $team]);
|
||||
|
||||
$project = Project::factory()->create(['team_id' => $team->id]);
|
||||
foreach (['deprecated', 'staging', 'qa', 'development', 'demo'] as $name) {
|
||||
$project->environments()->create(['name' => $name]);
|
||||
}
|
||||
|
||||
Livewire::test(ProjectShow::class, ['project_uuid' => $project->uuid])
|
||||
->assertViewHas('environmentsJs', fn ($environments) => collect($environments)->pluck('name')->all() === ['production', 'staging', 'development', 'demo', 'deprecated', 'qa']);
|
||||
});
|
||||
Reference in New Issue
Block a user