diff options
Diffstat (limited to 'spec/frontend/packages')
10 files changed, 370 insertions, 36 deletions
diff --git a/spec/frontend/packages/details/components/__snapshots__/file_sha_spec.js.snap b/spec/frontend/packages/details/components/__snapshots__/file_sha_spec.js.snap new file mode 100644 index 00000000000..881d441e116 --- /dev/null +++ b/spec/frontend/packages/details/components/__snapshots__/file_sha_spec.js.snap @@ -0,0 +1,30 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FileSha renders 1`] = ` +<div + class="gl-display-flex gl-align-items-center gl-font-monospace gl-font-sm gl-word-break-all gl-py-2 gl-border-b-solid gl-border-gray-100 gl-border-b-1" +> + <!----> + + <span> + <div + class="gl-px-4" + > + + bar: + foo + + <gl-button-stub + aria-label="Copy this value" + buttontextclasses="" + category="tertiary" + data-clipboard-text="foo" + icon="copy-to-clipboard" + size="small" + title="Copy SHA" + variant="default" + /> + </div> + </span> +</div> +`; diff --git a/spec/frontend/packages/details/components/app_spec.js b/spec/frontend/packages/details/components/app_spec.js index 11dad7ba34d..3132ec61942 100644 --- a/spec/frontend/packages/details/components/app_spec.js +++ b/spec/frontend/packages/details/components/app_spec.js @@ -1,5 +1,6 @@ -import { GlEmptyState, GlModal } from '@gitlab/ui'; +import { GlEmptyState } from '@gitlab/ui'; import { mount, createLocalVue } from '@vue/test-utils'; +import { nextTick } from 'vue'; import Vuex from 'vuex'; import stubChildren from 'helpers/stub_children'; @@ -34,6 +35,7 @@ describe('PackagesApp', () => { let store; const fetchPackageVersions = jest.fn(); const deletePackage = jest.fn(); + const deletePackageFile = jest.fn(); const defaultProjectName = 'bar'; const { location } = window; @@ -59,6 +61,7 @@ describe('PackagesApp', () => { actions: { deletePackage, fetchPackageVersions, + deletePackageFile, }, getters, }); @@ -82,8 +85,8 @@ describe('PackagesApp', () => { const packageTitle = () => wrapper.find(PackageTitle); const emptyState = () => wrapper.find(GlEmptyState); const deleteButton = () => wrapper.find('.js-delete-button'); - const deleteModal = () => wrapper.find(GlModal); - const modalDeleteButton = () => wrapper.find({ ref: 'modal-delete-button' }); + const findDeleteModal = () => wrapper.find({ ref: 'deleteModal' }); + const findDeleteFileModal = () => wrapper.find({ ref: 'deleteFileModal' }); const versionsTab = () => wrapper.find('.js-versions-tab > a'); const packagesLoader = () => wrapper.find(PackagesListLoader); const packagesVersionRows = () => wrapper.findAll(PackageListRow); @@ -107,10 +110,12 @@ describe('PackagesApp', () => { window.location = location; }); - it('renders the app and displays the package title', () => { + it('renders the app and displays the package title', async () => { createComponent(); - expect(packageTitle()).toExist(); + await nextTick(); + + expect(packageTitle().exists()).toBe(true); }); it('renders an empty state component when no an invalid package is passed as a prop', () => { @@ -118,7 +123,7 @@ describe('PackagesApp', () => { packageEntity: {}, }); - expect(emptyState()).toExist(); + expect(emptyState().exists()).toBe(true); }); it('package history has the right props', () => { @@ -152,7 +157,16 @@ describe('PackagesApp', () => { }); it('shows the delete confirmation modal when delete is clicked', () => { - expect(deleteModal()).toExist(); + expect(findDeleteModal().exists()).toBe(true); + }); + }); + + describe('deleting package files', () => { + it('shows the delete confirmation modal when delete is clicked', () => { + createComponent(); + findPackageFiles().vm.$emit('delete-file', mavenFiles[0]); + + expect(findDeleteFileModal().exists()).toBe(true); }); }); @@ -228,13 +242,7 @@ describe('PackagesApp', () => { }); describe('tracking and delete', () => { - const doDelete = async () => { - deleteButton().trigger('click'); - await wrapper.vm.$nextTick(); - modalDeleteButton().trigger('click'); - }; - - describe('delete', () => { + describe('delete package', () => { const originalReferrer = document.referrer; const setReferrer = (value = defaultProjectName) => { Object.defineProperty(document, 'referrer', { @@ -250,9 +258,9 @@ describe('PackagesApp', () => { }); }); - it('calls the proper vuex action', async () => { + it('calls the proper vuex action', () => { createComponent({ packageEntity: npmPackage }); - await doDelete(); + findDeleteModal().vm.$emit('primary'); expect(deletePackage).toHaveBeenCalled(); }); @@ -260,7 +268,7 @@ describe('PackagesApp', () => { setReferrer(); deletePackage.mockResolvedValue(); createComponent({ packageEntity: npmPackage }); - await doDelete(); + findDeleteModal().vm.$emit('primary'); await deletePackage(); expect(window.location.replace).toHaveBeenCalledWith( 'project_url?showSuccessDeleteAlert=true', @@ -271,7 +279,7 @@ describe('PackagesApp', () => { setReferrer('baz'); deletePackage.mockResolvedValue(); createComponent({ packageEntity: npmPackage }); - await doDelete(); + findDeleteModal().vm.$emit('primary'); await deletePackage(); expect(window.location.replace).toHaveBeenCalledWith( 'group_url?showSuccessDeleteAlert=true', @@ -279,6 +287,17 @@ describe('PackagesApp', () => { }); }); + describe('delete file', () => { + it('calls the proper vuex action', () => { + createComponent({ packageEntity: npmPackage }); + + findPackageFiles().vm.$emit('delete-file', mavenFiles[0]); + findDeleteFileModal().vm.$emit('primary'); + + expect(deletePackageFile).toHaveBeenCalled(); + }); + }); + describe('tracking', () => { let eventSpy; let utilSpy; @@ -295,9 +314,9 @@ describe('PackagesApp', () => { expect(utilSpy).toHaveBeenCalledWith('conan'); }); - it(`delete button on delete modal call event with ${TrackingActions.DELETE_PACKAGE}`, async () => { + it(`delete button on delete modal call event with ${TrackingActions.DELETE_PACKAGE}`, () => { createComponent({ packageEntity: npmPackage }); - await doDelete(); + findDeleteModal().vm.$emit('primary'); expect(eventSpy).toHaveBeenCalledWith( category, TrackingActions.DELETE_PACKAGE, @@ -305,6 +324,56 @@ describe('PackagesApp', () => { ); }); + it(`canceling a package deletion tracks ${TrackingActions.CANCEL_DELETE_PACKAGE}`, () => { + createComponent({ packageEntity: npmPackage }); + + findDeleteModal().vm.$emit('canceled'); + + expect(eventSpy).toHaveBeenCalledWith( + category, + TrackingActions.CANCEL_DELETE_PACKAGE, + expect.any(Object), + ); + }); + + it(`request a file deletion tracks ${TrackingActions.REQUEST_DELETE_PACKAGE_FILE}`, () => { + createComponent({ packageEntity: npmPackage }); + + findPackageFiles().vm.$emit('delete-file', mavenFiles[0]); + + expect(eventSpy).toHaveBeenCalledWith( + category, + TrackingActions.REQUEST_DELETE_PACKAGE_FILE, + expect.any(Object), + ); + }); + + it(`confirming a file deletion tracks ${TrackingActions.DELETE_PACKAGE_FILE}`, () => { + createComponent({ packageEntity: npmPackage }); + + findPackageFiles().vm.$emit('delete-file', npmPackage); + findDeleteFileModal().vm.$emit('primary'); + + expect(eventSpy).toHaveBeenCalledWith( + category, + TrackingActions.REQUEST_DELETE_PACKAGE_FILE, + expect.any(Object), + ); + }); + + it(`canceling a file deletion tracks ${TrackingActions.CANCEL_DELETE_PACKAGE_FILE}`, () => { + createComponent({ packageEntity: npmPackage }); + + findPackageFiles().vm.$emit('delete-file', npmPackage); + findDeleteFileModal().vm.$emit('canceled'); + + expect(eventSpy).toHaveBeenCalledWith( + category, + TrackingActions.CANCEL_DELETE_PACKAGE_FILE, + expect.any(Object), + ); + }); + it(`file download link call event with ${TrackingActions.PULL_PACKAGE}`, () => { createComponent({ packageEntity: conanPackage }); diff --git a/spec/frontend/packages/details/components/file_sha_spec.js b/spec/frontend/packages/details/components/file_sha_spec.js new file mode 100644 index 00000000000..7bfcf78baab --- /dev/null +++ b/spec/frontend/packages/details/components/file_sha_spec.js @@ -0,0 +1,33 @@ +import { shallowMount } from '@vue/test-utils'; + +import FileSha from '~/packages/details/components/file_sha.vue'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import DetailsRow from '~/vue_shared/components/registry/details_row.vue'; + +describe('FileSha', () => { + let wrapper; + + const defaultProps = { sha: 'foo', title: 'bar' }; + + function createComponent() { + wrapper = shallowMount(FileSha, { + propsData: { + ...defaultProps, + }, + stubs: { + ClipboardButton, + DetailsRow, + }, + }); + } + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders', () => { + createComponent(); + + expect(wrapper.element).toMatchSnapshot(); + }); +}); diff --git a/spec/frontend/packages/details/components/installations_commands_spec.js b/spec/frontend/packages/details/components/installations_commands_spec.js index 065bf503585..164f9f69741 100644 --- a/spec/frontend/packages/details/components/installations_commands_spec.js +++ b/spec/frontend/packages/details/components/installations_commands_spec.js @@ -7,6 +7,7 @@ import MavenInstallation from '~/packages/details/components/maven_installation. import NpmInstallation from '~/packages/details/components/npm_installation.vue'; import NugetInstallation from '~/packages/details/components/nuget_installation.vue'; import PypiInstallation from '~/packages/details/components/pypi_installation.vue'; +import TerraformInstallation from '~/packages_and_registries/infrastructure_registry/components/terraform_installation.vue'; import { conanPackage, @@ -15,6 +16,7 @@ import { nugetPackage, pypiPackage, composerPackage, + terraformModule, } from '../../mock_data'; describe('InstallationCommands', () => { @@ -32,6 +34,7 @@ describe('InstallationCommands', () => { const nugetInstallation = () => wrapper.find(NugetInstallation); const pypiInstallation = () => wrapper.find(PypiInstallation); const composerInstallation = () => wrapper.find(ComposerInstallation); + const terraformInstallation = () => wrapper.findComponent(TerraformInstallation); afterEach(() => { wrapper.destroy(); @@ -46,6 +49,7 @@ describe('InstallationCommands', () => { ${nugetPackage} | ${nugetInstallation} ${pypiPackage} | ${pypiInstallation} ${composerPackage} | ${composerInstallation} + ${terraformModule} | ${terraformInstallation} `('renders', ({ packageEntity, selector }) => { it(`${packageEntity.package_type} instructions exist`, () => { createComponent({ packageEntity }); diff --git a/spec/frontend/packages/details/components/package_files_spec.js b/spec/frontend/packages/details/components/package_files_spec.js index bcf1b6d56f0..e8e5a24d3a3 100644 --- a/spec/frontend/packages/details/components/package_files_spec.js +++ b/spec/frontend/packages/details/components/package_files_spec.js @@ -1,4 +1,6 @@ +import { GlDropdown, GlButton } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; +import { nextTick } from 'vue/'; import stubChildren from 'helpers/stub_children'; import component from '~/packages/details/components/package_files.vue'; import FileIcon from '~/vue_shared/components/file_icon.vue'; @@ -12,16 +14,21 @@ describe('Package Files', () => { const findAllRows = () => wrapper.findAll('[data-testid="file-row"'); const findFirstRow = () => findAllRows().at(0); const findSecondRow = () => findAllRows().at(1); - const findFirstRowDownloadLink = () => findFirstRow().find('[data-testid="download-link"'); - const findFirstRowCommitLink = () => findFirstRow().find('[data-testid="commit-link"'); - const findSecondRowCommitLink = () => findSecondRow().find('[data-testid="commit-link"'); + const findFirstRowDownloadLink = () => findFirstRow().find('[data-testid="download-link"]'); + const findFirstRowCommitLink = () => findFirstRow().find('[data-testid="commit-link"]'); + const findSecondRowCommitLink = () => findSecondRow().find('[data-testid="commit-link"]'); const findFirstRowFileIcon = () => findFirstRow().find(FileIcon); const findFirstRowCreatedAt = () => findFirstRow().find(TimeAgoTooltip); + const findFirstActionMenu = () => findFirstRow().findComponent(GlDropdown); + const findActionMenuDelete = () => findFirstActionMenu().find('[data-testid="delete-file"]'); + const findFirstToggleDetailsButton = () => findFirstRow().findComponent(GlButton); + const findFirstRowShaComponent = (id) => wrapper.find(`[data-testid="${id}"]`); - const createComponent = (packageFiles = npmFiles) => { + const createComponent = ({ packageFiles = npmFiles, canDelete = true } = {}) => { wrapper = mount(component, { propsData: { packageFiles, + canDelete, }, stubs: { ...stubChildren(component), @@ -43,7 +50,7 @@ describe('Package Files', () => { }); it('renders multiple files for a package that contains more than one file', () => { - createComponent(mavenFiles); + createComponent({ packageFiles: mavenFiles }); expect(findAllRows()).toHaveLength(2); }); @@ -123,7 +130,7 @@ describe('Package Files', () => { }); describe('when package file has no pipeline associated', () => { it('does not exist', () => { - createComponent(mavenFiles); + createComponent({ packageFiles: mavenFiles }); expect(findFirstRowCommitLink().exists()).toBe(false); }); @@ -131,11 +138,122 @@ describe('Package Files', () => { describe('when only one file lacks an associated pipeline', () => { it('renders the commit when it exists and not otherwise', () => { - createComponent([npmFiles[0], mavenFiles[0]]); + createComponent({ packageFiles: [npmFiles[0], mavenFiles[0]] }); expect(findFirstRowCommitLink().exists()).toBe(true); expect(findSecondRowCommitLink().exists()).toBe(false); }); }); + + describe('action menu', () => { + describe('when the user can delete', () => { + it('exists', () => { + createComponent(); + + expect(findFirstActionMenu().exists()).toBe(true); + }); + + describe('menu items', () => { + describe('delete file', () => { + it('exists', () => { + createComponent(); + + expect(findActionMenuDelete().exists()).toBe(true); + }); + + it('emits a delete event when clicked', () => { + createComponent(); + + findActionMenuDelete().vm.$emit('click'); + + const [[{ id }]] = wrapper.emitted('delete-file'); + expect(id).toBe(npmFiles[0].id); + }); + }); + }); + }); + + describe('when the user can not delete', () => { + const canDelete = false; + + it('does not exist', () => { + createComponent({ canDelete }); + + expect(findFirstActionMenu().exists()).toBe(false); + }); + }); + }); + }); + + describe('additional details', () => { + describe('details toggle button', () => { + it('exists', () => { + createComponent(); + + expect(findFirstToggleDetailsButton().exists()).toBe(true); + }); + + it('is hidden when no details is present', () => { + const [{ ...noShaFile }] = npmFiles; + noShaFile.file_sha256 = null; + noShaFile.file_md5 = null; + noShaFile.file_sha1 = null; + createComponent({ packageFiles: [noShaFile] }); + + expect(findFirstToggleDetailsButton().exists()).toBe(false); + }); + + it('toggles the details row', async () => { + createComponent(); + + expect(findFirstToggleDetailsButton().props('icon')).toBe('angle-down'); + + findFirstToggleDetailsButton().vm.$emit('click'); + await nextTick(); + + expect(findFirstRowShaComponent('sha-256').exists()).toBe(true); + expect(findFirstToggleDetailsButton().props('icon')).toBe('angle-up'); + + findFirstToggleDetailsButton().vm.$emit('click'); + await nextTick(); + + expect(findFirstRowShaComponent('sha-256').exists()).toBe(false); + expect(findFirstToggleDetailsButton().props('icon')).toBe('angle-down'); + }); + }); + + describe('file shas', () => { + const showShaFiles = () => { + findFirstToggleDetailsButton().vm.$emit('click'); + return nextTick(); + }; + + it.each` + selector | title | sha + ${'sha-256'} | ${'SHA-256'} | ${'file_sha256'} + ${'md5'} | ${'MD5'} | ${'file_md5'} + ${'sha-1'} | ${'SHA-1'} | ${'file_sha1'} + `('has a $title row', async ({ selector, title, sha }) => { + createComponent(); + + await showShaFiles(); + + expect(findFirstRowShaComponent(selector).props()).toMatchObject({ + title, + sha, + }); + }); + + it('does not display a row when the data is missing', async () => { + const [{ ...missingMd5 }] = npmFiles; + missingMd5.file_md5 = null; + + createComponent({ packageFiles: [missingMd5] }); + + await showShaFiles(); + + expect(findFirstRowShaComponent('md5').exists()).toBe(false); + }); + }); }); }); diff --git a/spec/frontend/packages/details/store/actions_spec.js b/spec/frontend/packages/details/store/actions_spec.js index d11ee548b72..b16e50debc4 100644 --- a/spec/frontend/packages/details/store/actions_spec.js +++ b/spec/frontend/packages/details/store/actions_spec.js @@ -1,10 +1,18 @@ import testAction from 'helpers/vuex_action_helper'; import Api from '~/api'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import { FETCH_PACKAGE_VERSIONS_ERROR } from '~/packages/details/constants'; -import { fetchPackageVersions, deletePackage } from '~/packages/details/store/actions'; +import { + fetchPackageVersions, + deletePackage, + deletePackageFile, +} from '~/packages/details/store/actions'; import * as types from '~/packages/details/store/mutation_types'; -import { DELETE_PACKAGE_ERROR_MESSAGE } from '~/packages/shared/constants'; +import { + DELETE_PACKAGE_ERROR_MESSAGE, + DELETE_PACKAGE_FILE_ERROR_MESSAGE, + DELETE_PACKAGE_FILE_SUCCESS_MESSAGE, +} from '~/packages/shared/constants'; import { npmPackage as packageEntity } from '../../mock_data'; jest.mock('~/flash.js'); @@ -74,7 +82,10 @@ describe('Actions Package details store', () => { packageEntity.project_id, packageEntity.id, ); - expect(createFlash).toHaveBeenCalledWith(FETCH_PACKAGE_VERSIONS_ERROR); + expect(createFlash).toHaveBeenCalledWith({ + message: FETCH_PACKAGE_VERSIONS_ERROR, + type: 'warning', + }); done(); }, ); @@ -96,7 +107,48 @@ describe('Actions Package details store', () => { Api.deleteProjectPackage = jest.fn().mockRejectedValue(); testAction(deletePackage, undefined, { packageEntity }, [], [], () => { - expect(createFlash).toHaveBeenCalledWith(DELETE_PACKAGE_ERROR_MESSAGE); + expect(createFlash).toHaveBeenCalledWith({ + message: DELETE_PACKAGE_ERROR_MESSAGE, + type: 'warning', + }); + done(); + }); + }); + }); + + describe('deletePackageFile', () => { + const fileId = 'a_file_id'; + + it('should call Api.deleteProjectPackageFile and commit the right data', (done) => { + const packageFiles = [{ id: 'foo' }, { id: fileId }]; + Api.deleteProjectPackageFile = jest.fn().mockResolvedValue(); + testAction( + deletePackageFile, + fileId, + { packageEntity, packageFiles }, + [{ type: types.UPDATE_PACKAGE_FILES, payload: [{ id: 'foo' }] }], + [], + () => { + expect(Api.deleteProjectPackageFile).toHaveBeenCalledWith( + packageEntity.project_id, + packageEntity.id, + fileId, + ); + expect(createFlash).toHaveBeenCalledWith({ + message: DELETE_PACKAGE_FILE_SUCCESS_MESSAGE, + type: 'success', + }); + done(); + }, + ); + }); + it('should create flash on API error', (done) => { + Api.deleteProjectPackageFile = jest.fn().mockRejectedValue(); + testAction(deletePackageFile, fileId, { packageEntity }, [], [], () => { + expect(createFlash).toHaveBeenCalledWith({ + message: DELETE_PACKAGE_FILE_ERROR_MESSAGE, + type: 'warning', + }); done(); }); }); diff --git a/spec/frontend/packages/details/store/mutations_spec.js b/spec/frontend/packages/details/store/mutations_spec.js index 6bc5fb7241f..296ed02d786 100644 --- a/spec/frontend/packages/details/store/mutations_spec.js +++ b/spec/frontend/packages/details/store/mutations_spec.js @@ -28,4 +28,13 @@ describe('Mutations package details Store', () => { expect(mockState.packageEntity.versions).toEqual(fakeVersions); }); }); + describe('UPDATE_PACKAGE_FILES', () => { + it('should update the packageFiles', () => { + const files = [1, 2, 3]; + + mutations[types.UPDATE_PACKAGE_FILES](mockState, files); + + expect(mockState.packageFiles).toEqual(files); + }); + }); }); diff --git a/spec/frontend/packages/list/stores/actions_spec.js b/spec/frontend/packages/list/stores/actions_spec.js index 52966c1be5e..adccb7436e1 100644 --- a/spec/frontend/packages/list/stores/actions_spec.js +++ b/spec/frontend/packages/list/stores/actions_spec.js @@ -2,7 +2,7 @@ import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import testAction from 'helpers/vuex_action_helper'; import Api from '~/api'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import { MISSING_DELETE_PATH_ERROR } from '~/packages/list/constants'; import * as actions from '~/packages/list/stores/actions'; import * as types from '~/packages/list/stores/mutation_types'; @@ -241,7 +241,9 @@ describe('Actions Package list store', () => { `('should reject and createFlash when $property is missing', ({ actionPayload }, done) => { testAction(actions.requestDeletePackage, actionPayload, null, [], []).catch((e) => { expect(e).toEqual(new Error(MISSING_DELETE_PATH_ERROR)); - expect(createFlash).toHaveBeenCalledWith(DELETE_PACKAGE_ERROR_MESSAGE); + expect(createFlash).toHaveBeenCalledWith({ + message: DELETE_PACKAGE_ERROR_MESSAGE, + }); done(); }); }); diff --git a/spec/frontend/packages/mock_data.js b/spec/frontend/packages/mock_data.js index 06009daba54..33b47cca68b 100644 --- a/spec/frontend/packages/mock_data.js +++ b/spec/frontend/packages/mock_data.js @@ -79,6 +79,9 @@ export const npmFiles = [ pipelines: [ { id: 1, project: { commit_url: 'http://foo.bar' }, git_commit_message: 'foo bar baz?' }, ], + file_sha256: 'file_sha256', + file_md5: 'file_md5', + file_sha1: 'file_sha1', }, ]; @@ -175,6 +178,20 @@ export const composerPackage = { version: '1.0.0', }; +export const terraformModule = { + created_at: '2015-12-10', + id: 2, + name: 'Test/system-22', + package_type: 'terraform_module', + project_path: 'foo/bar/baz', + projectPathName: 'foo/bar/baz', + project_id: 1, + updated_at: '2015-12-10', + version: '0.1', + versions: [], + _links, +}; + export const mockTags = [ { name: 'foo-1', diff --git a/spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap b/spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap index f4e617ecafe..b576f1b2553 100644 --- a/spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap +++ b/spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap @@ -11,7 +11,7 @@ exports[`packages_list_row renders 1`] = ` <!----> <div - class="gl-display-flex gl-xs-flex-direction-column gl-justify-content-space-between gl-align-items-stretch gl-flex-fill-1" + class="gl-display-flex gl-xs-flex-direction-column gl-justify-content-space-between gl-align-items-stretch gl-flex-grow-1" > <div class="gl-display-flex gl-flex-direction-column gl-xs-mb-3 gl-min-w-0 gl-flex-grow-1" @@ -42,7 +42,7 @@ exports[`packages_list_row renders 1`] = ` </div> <div - class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-min-h-6 gl-min-w-0 gl-flex-fill-1" + class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-min-h-6 gl-min-w-0 gl-flex-grow-1" > <div class="gl-display-flex" |