diff --git a/package-lock.json b/package-lock.json index 90bfb78b1..e212703a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2771,8 +2771,7 @@ } }, "incompatible-packages": { - "version": "https://www.atom.io/api/packages/incompatible-packages/versions/0.27.3/tarball", - "integrity": "sha512-OlkFBSpvHH7dUfYQTlcgTXEa+sjr9Es8d2lNPGPS2O5Rp5MiRKcnovQoMtaF3fkcuV2O7onim45ldFjbl4qdog==", + "version": "file:packages/incompatible-packages", "requires": { "etch": "^0.12.2" } diff --git a/package.json b/package.json index 354a49f22..a6e89b59d 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "grammar-selector": "https://www.atom.io/api/packages/grammar-selector/versions/0.50.1/tarball", "grim": "1.5.0", "image-view": "https://www.atom.io/api/packages/image-view/versions/0.63.1/tarball", - "incompatible-packages": "https://www.atom.io/api/packages/incompatible-packages/versions/0.27.3/tarball", + "incompatible-packages": "file:packages/incompatible-packages", "jasmine-json": "~0.0", "jasmine-reporters": "1.1.0", "jasmine-tagged": "^1.1.4", @@ -207,7 +207,7 @@ "go-to-line": "0.33.0", "grammar-selector": "0.50.1", "image-view": "0.63.1", - "incompatible-packages": "0.27.3", + "incompatible-packages": "file:./packages/incompatible-packages", "keybinding-resolver": "0.38.4", "line-ending-selector": "0.7.7", "link": "0.31.6", diff --git a/packages/README.md b/packages/README.md index 0917a2541..b88d01d34 100644 --- a/packages/README.md +++ b/packages/README.md @@ -38,7 +38,7 @@ See [RFC 003](https://github.com/atom/atom/blob/master/docs/rfcs/003-consolidate | **go-to-line** | [`atom/go-to-line`][go-to-line] | [#17844](https://github.com/atom/atom/issues/17844) | | **grammar-selector** | [`atom/grammar-selector`][grammar-selector] | [#17845](https://github.com/atom/atom/issues/17845) | | **image-view** | [`atom/image-view`][image-view] | | -| **incompatible-packages** | [`atom/incompatible-packages`][incompatible-packages] | [#17846](https://github.com/atom/atom/issues/17846) | +| **incompatible-packages** | [`./packages/incompatible-packages`][./incompatible-packages] | [#17846](https://github.com/atom/atom/issues/17846) | | **keybinding-resolver** | [`atom/keybinding-resolver`][keybinding-resolver] | | | **language-c** | [`atom/language-c`][language-c] | | | **language-clojure** | [`atom/language-clojure`][language-clojure] | | diff --git a/packages/incompatible-packages/.gitignore b/packages/incompatible-packages/.gitignore new file mode 100644 index 000000000..ade14b919 --- /dev/null +++ b/packages/incompatible-packages/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +npm-debug.log +node_modules diff --git a/packages/incompatible-packages/LICENSE.md b/packages/incompatible-packages/LICENSE.md new file mode 100644 index 000000000..4d231b456 --- /dev/null +++ b/packages/incompatible-packages/LICENSE.md @@ -0,0 +1,20 @@ +Copyright (c) 2014 GitHub Inc. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/incompatible-packages/README.md b/packages/incompatible-packages/README.md new file mode 100644 index 000000000..43b92d7c4 --- /dev/null +++ b/packages/incompatible-packages/README.md @@ -0,0 +1,6 @@ +# Incompatible Packages package + +Displays a list of installed Atom packages that have native module +dependencies that are not compatible with the current version of Atom. + +![](https://cloud.githubusercontent.com/assets/671378/3767534/3f099820-18ce-11e4-9fa0-feef7947aab2.png) diff --git a/packages/incompatible-packages/lib/incompatible-packages-component.js b/packages/incompatible-packages/lib/incompatible-packages-component.js new file mode 100644 index 000000000..8eb6c62c6 --- /dev/null +++ b/packages/incompatible-packages/lib/incompatible-packages-component.js @@ -0,0 +1,228 @@ +/** @babel */ +/** @jsx etch.dom */ + +import {BufferedProcess} from 'atom' +import etch from 'etch' + +import VIEW_URI from './view-uri' +const REBUILDING = 'rebuilding' +const REBUILD_FAILED = 'rebuild-failed' +const REBUILD_SUCCEEDED = 'rebuild-succeeded' + +export default class IncompatiblePackagesComponent { + constructor (packageManager) { + this.rebuildStatuses = new Map + this.rebuildFailureOutputs = new Map + this.rebuildInProgress = false + this.rebuiltPackageCount = 0 + this.packageManager = packageManager + this.loaded = false + etch.initialize(this) + + if (this.packageManager.getActivePackages().length > 0) { + this.populateIncompatiblePackages() + } else { + global.setImmediate(this.populateIncompatiblePackages.bind(this)) + } + + this.element.addEventListener('click', (event) => { + if (event.target === this.refs.rebuildButton) { + this.rebuildIncompatiblePackages() + } else if (event.target === this.refs.reloadButton) { + atom.reload() + } else if (event.target.classList.contains('view-settings')) { + atom.workspace.open(`atom://config/packages/${event.target.package.name}`) + } + }) + } + + update () {} + + render () { + if (!this.loaded) { + return
Loading...
+ } + + return ( +
+ {this.renderHeading()} + {this.renderIncompatiblePackageList()} +
+ ) + } + + renderHeading () { + if (this.incompatiblePackages.length > 0) { + if (this.rebuiltPackageCount > 0) { + let alertClass = + (this.rebuiltPackageCount === this.incompatiblePackages.length) + ? 'alert-success icon-check' + : 'alert-warning icon-bug' + + return ( +
+ {this.rebuiltPackageCount} of {this.incompatiblePackages.length} packages + were rebuilt successfully. Reload Atom to activate them. + + +
+ ) + } else { + return ( +
+ Some installed packages could not be loaded because they contain native + modules that were compiled for an earlier version of Atom. + + +
+ ) + } + } else { + return ( +
+ None of your packages contain incompatible native modules. +
+ ) + } + } + + renderIncompatiblePackageList () { + return ( +
{ + this.incompatiblePackages.map(this.renderIncompatiblePackage.bind(this)) + }
+ ) + } + + renderIncompatiblePackage (pack) { + let rebuildStatus = this.rebuildStatuses.get(pack) + + return ( +
+ {this.renderRebuildStatusIndicator(rebuildStatus)} + +

+ {pack.name} {pack.metadata.version} +

+ { + rebuildStatus + ? this.renderRebuildOutput(pack) + : this.renderIncompatibleModules(pack) + } +
+ ) + } + + renderRebuildStatusIndicator (rebuildStatus) { + if (rebuildStatus === REBUILDING) { + return ( +
+ Rebuilding +
+ ) + } else if (rebuildStatus === REBUILD_SUCCEEDED) { + return ( +
+ Rebuild Succeeded +
+ ) + } else if (rebuildStatus === REBUILD_FAILED) { + return ( +
+ Rebuild Failed +
+ ) + } else { + return '' + } + } + + renderRebuildOutput (pack) { + if (this.rebuildStatuses.get(pack) === REBUILD_FAILED) { + return
{this.rebuildFailureOutputs.get(pack)}
+ } else { + return '' + } + } + + renderIncompatibleModules (pack) { + return ( + + ) + } + + populateIncompatiblePackages () { + this.incompatiblePackages = + this.packageManager + .getLoadedPackages() + .filter(pack => !pack.isCompatible()) + + for (let pack of this.incompatiblePackages) { + let buildFailureOutput = pack.getBuildFailureOutput() + if (buildFailureOutput) { + this.setPackageStatus(pack, REBUILD_FAILED) + this.setRebuildFailureOutput(pack, buildFailureOutput) + } + } + + this.loaded = true + etch.update(this) + } + + async rebuildIncompatiblePackages () { + this.rebuildInProgress = true + let rebuiltPackageCount = 0 + for (let pack of this.incompatiblePackages) { + this.setPackageStatus(pack, REBUILDING) + let {code, stderr} = await pack.rebuild() + if (code === 0) { + this.setPackageStatus(pack, REBUILD_SUCCEEDED) + rebuiltPackageCount++ + } else { + this.setRebuildFailureOutput(pack, stderr) + this.setPackageStatus(pack, REBUILD_FAILED) + } + } + this.rebuildInProgress = false + this.rebuiltPackageCount = rebuiltPackageCount + etch.update(this) + } + + setPackageStatus (pack, status) { + this.rebuildStatuses.set(pack, status) + etch.update(this) + } + + setRebuildFailureOutput (pack, output) { + this.rebuildFailureOutputs.set(pack, output) + etch.update(this) + } + + getTitle () { + return 'Incompatible Packages' + } + + getURI () { + return VIEW_URI + } + + getIconName () { + return 'package' + } + + serialize () { + return {deserializer: 'IncompatiblePackagesComponent'} + } +} diff --git a/packages/incompatible-packages/lib/main.js b/packages/incompatible-packages/lib/main.js new file mode 100644 index 000000000..b936cb880 --- /dev/null +++ b/packages/incompatible-packages/lib/main.js @@ -0,0 +1,52 @@ +/** @babel */ + +import {Disposable, CompositeDisposable} from 'atom' +import VIEW_URI from './view-uri' + +let disposables = null + +export function activate () { + disposables = new CompositeDisposable() + + disposables.add(atom.workspace.addOpener((uri) => { + if (uri === VIEW_URI) { + return deserializeIncompatiblePackagesComponent() + } + })) + + disposables.add(atom.commands.add('atom-workspace', { + 'incompatible-packages:view': () => { + atom.workspace.open(VIEW_URI) + } + })) +} + +export function deactivate () { + disposables.dispose() +} + +export function consumeStatusBar (statusBar) { + let incompatibleCount = 0 + for (let pack of atom.packages.getLoadedPackages()) { + if (!pack.isCompatible()) incompatibleCount++ + } + + if (incompatibleCount > 0) { + let icon = createIcon(incompatibleCount) + let tile = statusBar.addRightTile({item: icon, priority: 200}) + icon.element.addEventListener('click', () => { + atom.commands.dispatch(icon.element, 'incompatible-packages:view') + }) + disposables.add(new Disposable(() => tile.destroy())) + } +} + +export function deserializeIncompatiblePackagesComponent () { + const IncompatiblePackagesComponent = require('./incompatible-packages-component') + return new IncompatiblePackagesComponent(atom.packages) +} + +function createIcon (count) { + const StatusIconComponent = require('./status-icon-component') + return new StatusIconComponent({count}) +} diff --git a/packages/incompatible-packages/lib/status-icon-component.js b/packages/incompatible-packages/lib/status-icon-component.js new file mode 100644 index 000000000..72653ca9b --- /dev/null +++ b/packages/incompatible-packages/lib/status-icon-component.js @@ -0,0 +1,22 @@ +/** @babel */ +/** @jsx etch.dom */ + +import etch from 'etch' + +export default class StatusIconComponent { + constructor ({count}) { + this.count = count + etch.initialize(this) + } + + update () {} + + render () { + return ( +
+ + {this.count} +
+ ) + } +} diff --git a/packages/incompatible-packages/lib/view-uri.js b/packages/incompatible-packages/lib/view-uri.js new file mode 100644 index 000000000..de66f3ac6 --- /dev/null +++ b/packages/incompatible-packages/lib/view-uri.js @@ -0,0 +1,3 @@ +/** @babel */ + +export default 'atom://incompatible-packages' diff --git a/packages/incompatible-packages/package.json b/packages/incompatible-packages/package.json new file mode 100644 index 000000000..170a69c8c --- /dev/null +++ b/packages/incompatible-packages/package.json @@ -0,0 +1,24 @@ +{ + "name": "incompatible-packages", + "main": "./lib/main", + "version": "0.27.3", + "description": "Show incompatible packages", + "repository": "https://github.com/atom/atom", + "license": "MIT", + "engines": { + "atom": ">0.50.0" + }, + "dependencies": { + "etch": "^0.12.2" + }, + "consumedServices": { + "status-bar": { + "versions": { + "^1.0.0": "consumeStatusBar" + } + } + }, + "deserializers": { + "IncompatiblePackagesComponent": "deserializeIncompatiblePackagesComponent" + } +} diff --git a/packages/incompatible-packages/spec/fixtures/incompatible-package/bad.js b/packages/incompatible-packages/spec/fixtures/incompatible-package/bad.js new file mode 100644 index 000000000..e69de29bb diff --git a/packages/incompatible-packages/spec/fixtures/incompatible-package/package.json b/packages/incompatible-packages/spec/fixtures/incompatible-package/package.json new file mode 100644 index 000000000..bb8aeca55 --- /dev/null +++ b/packages/incompatible-packages/spec/fixtures/incompatible-package/package.json @@ -0,0 +1,5 @@ +{ + "name": "incompatible-package", + "version": "1.0.0", + "main": "./bad.js" +} diff --git a/packages/incompatible-packages/spec/incompatible-packages-component-spec.js b/packages/incompatible-packages/spec/incompatible-packages-component-spec.js new file mode 100644 index 000000000..88d46677e --- /dev/null +++ b/packages/incompatible-packages/spec/incompatible-packages-component-spec.js @@ -0,0 +1,243 @@ +/** @babel */ + +import etch from 'etch' +import IncompatiblePackagesComponent from '../lib/incompatible-packages-component' + +describe('IncompatiblePackagesComponent', () => { + let packages, etchScheduler + + beforeEach(() => { + etchScheduler = etch.getScheduler() + + packages = [ + { + name: 'incompatible-1', + isCompatible () { + return false + }, + rebuild: function () { + return new Promise((resolve) => this.resolveRebuild = resolve) + }, + getBuildFailureOutput () { + return null + }, + path: '/Users/joe/.atom/packages/incompatible-1', + metadata: { + repository: 'https://github.com/atom/incompatible-1', + version: '1.0.0' + }, + incompatibleModules: [ + {name: 'x', version: '1.0.0', error: 'Expected version X, got Y'}, + {name: 'y', version: '1.0.0', error: 'Expected version X, got Z'} + ] + }, + { + name: 'incompatible-2', + isCompatible () { + return false + }, + rebuild () { + return new Promise((resolve) => this.resolveRebuild = resolve) + }, + getBuildFailureOutput () { + return null + }, + path: '/Users/joe/.atom/packages/incompatible-2', + metadata: { + repository: 'https://github.com/atom/incompatible-2', + version: '1.0.0' + }, + incompatibleModules: [ + {name: 'z', version: '1.0.0', error: 'Expected version X, got Y'} + ] + }, + { + name: 'compatible', + isCompatible () { + return true + }, + rebuild () { + throw new Error('Should not rebuild a compatible package') + }, + getBuildFailureOutput () { + return null + }, + path: '/Users/joe/.atom/packages/b', + metadata: { + repository: 'https://github.com/atom/b', + version: '1.0.0' + }, + incompatibleModules: [], + } + ] + }) + + describe('when packages have not finished loading', () => { + it('delays rendering incompatible packages until the end of the tick', () => { + waitsForPromise(async () => { + let component = + new IncompatiblePackagesComponent({ + getActivePackages: () => [], + getLoadedPackages: () => packages + }) + let {element} = component + + expect(element.querySelectorAll('.incompatible-package').length).toEqual(0) + + await etchScheduler.getNextUpdatePromise() + + expect(element.querySelectorAll('.incompatible-package').length).toBeGreaterThan(0) + }) + }) + }) + + describe('when there are no incompatible packages', () => { + it('does not render incompatible packages or the rebuild button', () => { + waitsForPromise(async () => { + expect(packages[2].isCompatible()).toBe(true) + let compatiblePackages = [packages[2]] + + let component = + new IncompatiblePackagesComponent({ + getActivePackages: () => compatiblePackages, + getLoadedPackages: () => compatiblePackages + }) + let {element} = component + + await etchScheduler.getNextUpdatePromise() + + expect(element.querySelectorAll('.incompatible-package').length).toBe(0) + expect(element.querySelector('button')).toBeNull() + }) + }) + }) + + describe('when some packages previously failed to rebuild', () => { + it('renders them with failed build status and error output', () => { + waitsForPromise(async () => { + packages[1].getBuildFailureOutput = function () { + return 'The build failed' + } + + let component = + new IncompatiblePackagesComponent({ + getActivePackages: () => packages, + getLoadedPackages: () => packages + }) + let {element} = component + + await etchScheduler.getNextUpdatePromise() + let packageElement = element.querySelector('.incompatible-package:nth-child(2)') + + expect(packageElement.querySelector('.badge').textContent).toBe('Rebuild Failed') + expect(packageElement.querySelector('pre').textContent).toBe('The build failed') + }) + }) + }) + + describe('when there are incompatible packages', () => { + it('renders incompatible packages and the rebuild button', () => { + waitsForPromise(async () => { + let component = + new IncompatiblePackagesComponent({ + getActivePackages: () => packages, + getLoadedPackages: () => packages + }) + let {element} = component + + await etchScheduler.getNextUpdatePromise() + + expect(element.querySelectorAll('.incompatible-package').length).toEqual(2) + expect(element.querySelector('button')).not.toBeNull() + }) + }) + + describe('when the "Rebuild All" button is clicked', () => { + it('rebuilds every incompatible package, updating each package\'s view with status', () => { + waitsForPromise(async () => { + let component = + new IncompatiblePackagesComponent({ + getActivePackages: () => packages, + getLoadedPackages: () => packages + }) + let {element} = component + jasmine.attachToDOM(element) + + await etchScheduler.getNextUpdatePromise() + + component.refs.rebuildButton.dispatchEvent(new CustomEvent('click', {bubbles: true})) + await etchScheduler.getNextUpdatePromise() // view update + + expect(component.refs.rebuildButton.disabled).toBe(true) + + expect(packages[0].resolveRebuild).toBeDefined() + + expect(element.querySelector('.incompatible-package:nth-child(1) .badge').textContent).toBe('Rebuilding') + expect(element.querySelector('.incompatible-package:nth-child(2) .badge')).toBeNull() + + packages[0].resolveRebuild({code: 0}) // simulate rebuild success + await etchScheduler.getNextUpdatePromise() // view update + + expect(packages[1].resolveRebuild).toBeDefined() + + expect(element.querySelector('.incompatible-package:nth-child(1) .badge').textContent).toBe('Rebuild Succeeded') + expect(element.querySelector('.incompatible-package:nth-child(2) .badge').textContent).toBe('Rebuilding') + + packages[1].resolveRebuild({code: 12, stderr: 'This is an error from the test!'}) // simulate rebuild failure + await etchScheduler.getNextUpdatePromise() // view update + + expect(element.querySelector('.incompatible-package:nth-child(1) .badge').textContent).toBe('Rebuild Succeeded') + expect(element.querySelector('.incompatible-package:nth-child(2) .badge').textContent).toBe('Rebuild Failed') + expect(element.querySelector('.incompatible-package:nth-child(2) pre').textContent).toBe('This is an error from the test!') + }) + }) + + it('displays a prompt to reload Atom when the packages finish rebuilding', () => { + waitsForPromise(async () => { + let component = + new IncompatiblePackagesComponent({ + getActivePackages: () => packages, + getLoadedPackages: () => packages + }) + let {element} = component + jasmine.attachToDOM(element) + await etchScheduler.getNextUpdatePromise() // view update + + component.refs.rebuildButton.dispatchEvent(new CustomEvent('click', {bubbles: true})) + expect(packages[0].resolveRebuild({code: 0})) + await new Promise(global.setImmediate) + expect(packages[1].resolveRebuild({code: 0})) + + await etchScheduler.getNextUpdatePromise() // view update + + expect(component.refs.reloadButton).toBeDefined() + expect(element.querySelector('.alert').textContent).toMatch(/2 of 2/) + + spyOn(atom, 'reload') + component.refs.reloadButton.dispatchEvent(new CustomEvent('click', {bubbles: true})) + expect(atom.reload).toHaveBeenCalled() + }) + }) + }) + + describe('when the "Package Settings" button is clicked', () => { + it('opens the settings panel for the package', () => { + waitsForPromise(async () => { + let component = + new IncompatiblePackagesComponent({ + getActivePackages: () => packages, + getLoadedPackages: () => packages + }) + let {element} = component + jasmine.attachToDOM(element) + + await etchScheduler.getNextUpdatePromise() + + spyOn(atom.workspace, 'open') + element.querySelector('.incompatible-package:nth-child(2) button').dispatchEvent(new CustomEvent('click', {bubbles: true})) + expect(atom.workspace.open).toHaveBeenCalledWith('atom://config/packages/incompatible-2') + }) + }) + }) + }) +}) diff --git a/packages/incompatible-packages/spec/incompatible-packages-spec.js b/packages/incompatible-packages/spec/incompatible-packages-spec.js new file mode 100644 index 000000000..fa6e7e76a --- /dev/null +++ b/packages/incompatible-packages/spec/incompatible-packages-spec.js @@ -0,0 +1,77 @@ +/** @babel */ + +import path from 'path' +import IncompatiblePackagesComponent from '../lib/incompatible-packages-component' +import StatusIconComponent from '../lib/status-icon-component' + +// This exists only so that CI passes on both Atom 1.6 and Atom 1.8+. +function findStatusBar () { + if (typeof atom.workspace.getFooterPanels === 'function') { + const footerPanels = atom.workspace.getFooterPanels() + if (footerPanels.length > 0) { + return footerPanels[0].getItem() + } + } + + return atom.workspace.getBottomPanels()[0].getItem() +} + +describe('Incompatible packages', () => { + let statusBar + + beforeEach(() => { + atom.views.getView(atom.workspace) + + waitsForPromise(() => atom.packages.activatePackage('status-bar')) + + runs(() => { + statusBar = findStatusBar() + }) + }) + + describe('when there are packages with incompatible native modules', () => { + beforeEach(() => { + let incompatiblePackage = atom.packages.loadPackage( + path.join(__dirname, 'fixtures', 'incompatible-package') + ) + spyOn(incompatiblePackage, 'isCompatible').andReturn(false) + incompatiblePackage.incompatibleModules = [] + waitsForPromise(() => atom.packages.activatePackage("incompatible-packages")) + + waits(1) + }) + + it('adds an icon to the status bar', () => { + let statusBarIcon = statusBar.getRightTiles()[0].getItem() + expect(statusBarIcon.constructor).toBe(StatusIconComponent) + }) + + describe('clicking the icon', () => { + it('displays the incompatible packages view in a pane', () => { + let statusBarIcon = statusBar.getRightTiles()[0].getItem() + statusBarIcon.element.dispatchEvent(new MouseEvent('click')) + + let activePaneItem + waitsFor(() => (activePaneItem = atom.workspace.getActivePaneItem())) + + runs(() => { + expect(activePaneItem.constructor).toBe(IncompatiblePackagesComponent) + }) + }) + }) + }) + + describe('when there are no packages with incompatible native modules', () => { + beforeEach(() => { + waitsForPromise(() => atom.packages.activatePackage("incompatible-packages")) + }) + + it('does not add an icon to the status bar', () => { + let statusBarItemClasses = statusBar + .getRightTiles() + .map((tile) => tile.getItem().className) + + expect(statusBarItemClasses).not.toContain('incompatible-packages') + }) + }) +}) diff --git a/packages/incompatible-packages/styles/incompatible-packages.less b/packages/incompatible-packages/styles/incompatible-packages.less new file mode 100644 index 000000000..df6a5ff1e --- /dev/null +++ b/packages/incompatible-packages/styles/incompatible-packages.less @@ -0,0 +1,42 @@ +@import "ui-variables"; + +.incompatible-packages { + background-color: @pane-item-background-color; + overflow-y: scroll; + + .incompatible-package { + padding: 15px; + margin-bottom: 10px; + border-radius: 6px; + border: 1px solid #d1d1d2; + background-color: #fafafa; + overflow: hidden; + + .badge { + margin-left: 1em; + } + + .heading { + margin-top: 0px; + } + + ul { + padding-left: 1em; + } + + li { + list-style-type: none; + } + + pre { + margin-top: 2em; + max-height: 25em; + overflow: scroll; + color: @text-color-error; + } + } +} + +.incompatible-packages-status { + padding-left: 2px; +}